diff --git a/alcs-frontend/Dockerfile b/alcs-frontend/Dockerfile index 8b3552cf30..0e4e9569d5 100644 --- a/alcs-frontend/Dockerfile +++ b/alcs-frontend/Dockerfile @@ -5,14 +5,16 @@ FROM node:20-alpine AS build WORKDIR /app # Copy package.json file -COPY package.json . +COPY package.json package-lock.json ./ # Install dependencies -RUN npm install +RUN npm ci # Copy the source code to the /app directory COPY . . +ENV NODE_OPTIONS="--max-old-space-size=2048" + # Build the application RUN npm run build -- --output-path=dist --output-hashing=all diff --git a/alcs-frontend/nginx.conf b/alcs-frontend/nginx.conf index 6ccc98d57e..b7847867d7 100644 --- a/alcs-frontend/nginx.conf +++ b/alcs-frontend/nginx.conf @@ -19,7 +19,7 @@ http { add_header 'X-XSS-Protection' '1; mode=block'; add_header 'Strict-Transport-Security' 'max-age=31536000; includeSubDomains; preload'; add_header 'Cache-control' 'no-cache'; - add_header 'Content-Security-Policy' "default-src 'self';img-src 'self';style-src 'unsafe-inline' 'self';connect-src $ENABLED_CONNECT_SRC; font-src 'self' https://fonts.gstatic.com https://fonts.googleapis.com; base-uri 'self'; object-src https://nrs.objectstore.gov.bc.ca; frame-src https://alcs-metabase-test.apps.silver.devops.gov.bc.ca https://alcs-metabase-prod.apps.silver.devops.gov.bc.ca https://nrs.objectstore.gov.bc.ca;"; + add_header 'Content-Security-Policy' "default-src 'self'; img-src 'self'; style-src 'unsafe-inline' 'self'; connect-src $ENABLED_CONNECT_SRC; font-src 'self' https://fonts.gstatic.com https://fonts.googleapis.com; base-uri 'self'; object-src https://nrs.objectstore.gov.bc.ca; frame-src https://alcs-metabase-test.apps.silver.devops.gov.bc.ca https://alcs-metabase-prod.apps.silver.devops.gov.bc.ca https://nrs.objectstore.gov.bc.ca; script-src 'self' https://www2.gov.bc.ca"; add_header 'Permissions-Policy' 'camera=(), geolocation=(), microphone=()'; add_header 'Referrer-Policy' 'same-origin'; diff --git a/alcs-frontend/src/app/app.component.ts b/alcs-frontend/src/app/app.component.ts index fd3a584643..b9e3e83ade 100644 --- a/alcs-frontend/src/app/app.component.ts +++ b/alcs-frontend/src/app/app.component.ts @@ -7,4 +7,6 @@ import { Component } from '@angular/core'; }) export class AppComponent { title = 'alcs-frontend'; + + constructor() {} } diff --git a/alcs-frontend/src/app/features/board/board.component.ts b/alcs-frontend/src/app/features/board/board.component.ts index 0fd0d029fb..5ad6ca0e88 100644 --- a/alcs-frontend/src/app/features/board/board.component.ts +++ b/alcs-frontend/src/app/features/board/board.component.ts @@ -390,18 +390,18 @@ export class BoardComponent implements OnInit, OnDestroy { private mapPlanningReferralToCard(referral: PlanningReferralDto): CardData { return { - status: referral.card.status.code, + status: referral.card!.status.code, typeLabel: 'Planning Review', title: `${referral.planningReview.fileNumber} (${referral.planningReview.documentName})`, titleTooltip: referral.planningReview.type.label, - assignee: referral.card.assignee, - id: referral.card.uuid, + assignee: referral.card!.assignee, + id: referral.card!.uuid, labels: [referral.planningReview.type], cardType: CardType.PLAN, paused: false, - highPriority: referral.card.highPriority, - cardUuid: referral.card.uuid, - dateReceived: referral.card.createdAt, + highPriority: referral.card!.highPriority, + cardUuid: referral.card!.uuid, + dateReceived: referral.card!.createdAt, dueDate: referral.dueDate ? new Date(referral.dueDate) : undefined, decisionMeetings: referral.planningReview.meetings, showDueDate: true, diff --git a/alcs-frontend/src/app/features/board/dialogs/planning-review/create/create-planning-review-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/planning-review/create/create-planning-review-dialog.component.ts index c23c38c897..2feac68d92 100644 --- a/alcs-frontend/src/app/features/board/dialogs/planning-review/create/create-planning-review-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/planning-review/create/create-planning-review-dialog.component.ts @@ -91,7 +91,7 @@ export class CreatePlanningReviewDialogComponent implements OnInit, OnDestroy { this.dialogRef.close(true); if (res) { await this.router.navigate(this.activatedRoute.snapshot.url, { - queryParams: res.card.uuid && res.card.type ? { card: res.card.uuid, type: res.card.type } : {}, + queryParams: res.card && res.card.uuid && res.card.type ? { card: res.card.uuid, type: res.card.type } : {}, relativeTo: this.activatedRoute, }); } diff --git a/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.ts index 3da9313146..0289809c2d 100644 --- a/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.ts @@ -55,7 +55,7 @@ export class PlanningReviewDialogComponent extends CardDialogComponent implement ...this.data.planningReview.type, borderColor: this.data.planningReview.type.backgroundColor, }; - this.populateCardData(this.data.card); + this.populateCardData(this.data.card!); this.selectedRegion = this.data.planningReview.region.code; this.cardTitle = `${this.data.planningReview.fileNumber} (${this.data.planningReview.documentName})`; @@ -64,16 +64,16 @@ export class PlanningReviewDialogComponent extends CardDialogComponent implement } private async reload() { - const planningReferral = await this.planningReferralService.fetchByCardUuid(this.planningReferral.card.uuid); + const planningReferral = await this.planningReferralService.fetchByCardUuid(this.planningReferral.card!.uuid); if (planningReferral) { - await this.populateCardData(planningReferral.card); + await this.populateCardData(planningReferral.card!); } } async onBoardSelected(board: BoardWithFavourite) { this.selectedBoard = board.code; try { - await this.boardService.changeBoard(this.planningReferral.card.uuid, board.code); + await this.boardService.changeBoard(this.planningReferral.card!.uuid, board.code); const loadedBoard = await this.boardService.fetchBoardDetail(board.code); if (loadedBoard) { this.boardStatuses = loadedBoard.statuses; diff --git a/alcs-frontend/src/app/features/home/assigned/assigned.component.ts b/alcs-frontend/src/app/features/home/assigned/assigned.component.ts index c956801892..860b654887 100644 --- a/alcs-frontend/src/app/features/home/assigned/assigned.component.ts +++ b/alcs-frontend/src/app/features/home/assigned/assigned.component.ts @@ -98,11 +98,11 @@ export class AssignedComponent implements OnInit { this.planningReferrals = [ ...planningReferrals - .filter((r) => r.card.highPriority) + .filter((r) => r.card!.highPriority) .map((r) => this.mapPlanningReferral(r)) .sort((a, b) => a.date! - b.date!), ...planningReferrals - .filter((r) => !r.card.highPriority) + .filter((r) => !r.card!.highPriority) .map((r) => this.mapPlanningReferral(r)) .sort((a, b) => a.date! - b.date!), ]; @@ -133,16 +133,17 @@ export class AssignedComponent implements OnInit { this.applications.length + this.planningReferrals.length + this.noticeOfIntents.length + - this.notifications.length; + this.notifications.length + + this.inquiries.length; } private mapPlanningReferral(p: PlanningReferralDto): AssignedToMeFile { return { title: `${p.planningReview.fileNumber} (${p.planningReview.documentName})`, - type: p.card.type, - date: p.card.createdAt, - card: p.card, - highPriority: p.card.highPriority, + type: p.card!.type, + date: p.card!.createdAt, + card: p.card!, + highPriority: p.card!.highPriority, labels: [p.planningReview.type], }; } diff --git a/alcs-frontend/src/app/features/home/subtask/subtask.component.ts b/alcs-frontend/src/app/features/home/subtask/subtask.component.ts index c52f114f97..f5e1bf47a1 100644 --- a/alcs-frontend/src/app/features/home/subtask/subtask.component.ts +++ b/alcs-frontend/src/app/features/home/subtask/subtask.component.ts @@ -100,7 +100,10 @@ export class SubtaskComponent implements OnInit, OnDestroy { if (this.showAppAndNonApp) { this.totalSubtaskCount = - this.applicationSubtasks.length + this.planningReviewSubtasks.length + this.notificationSubtasks.length; + this.applicationSubtasks.length + + this.planningReviewSubtasks.length + + this.notificationSubtasks.length + + this.inquirySubtasks.length; } if (this.showAppAndNonApp && this.showNoi) { diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts index 216603ce5d..97ada1a8c9 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts @@ -69,7 +69,7 @@ export class DecisionDocumentUploadDialogComponent implements OnInit { async onSubmit() { const file = this.pendingFile; if (file) { - const renamedFile = new File([file], this.name.value + this.extension ?? file.name); + const renamedFile = new File([file], this.name.value + this.extension ?? file.name, { type: file.type }); this.isSaving = true; if (this.data.existingDocument) { await this.decisionService.deleteFile(this.data.decisionUuid, this.data.existingDocument.uuid); diff --git a/alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts b/alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts index 8cb4121902..0aa89857bc 100644 --- a/alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts +++ b/alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts @@ -66,7 +66,7 @@ export class DecisionDocumentUploadDialogComponent implements OnInit { async onSubmit() { const file = this.pendingFile; if (file) { - const renamedFile = new File([file], this.name.value + this.extension ?? file.name); + const renamedFile = new File([file], this.name.value + this.extension ?? file.name, { type: file.type }); this.isSaving = true; if (this.data.existingDocument) { await this.decisionService.deleteFile(this.data.decisionUuid, this.data.existingDocument.uuid); diff --git a/alcs-frontend/src/app/features/planning-review/header/header.component.ts b/alcs-frontend/src/app/features/planning-review/header/header.component.ts index 488602c79d..2646bcb6f4 100644 --- a/alcs-frontend/src/app/features/planning-review/header/header.component.ts +++ b/alcs-frontend/src/app/features/planning-review/header/header.component.ts @@ -32,10 +32,12 @@ export class HeaderComponent implements OnChanges { async setupLinkedCards() { if ('referrals' in this.planningReview) { for (const [index, referral] of this.planningReview.referrals.entries()) { - this.linkedCards.push({ - ...referral.card, - displayName: `Referral #${this.planningReview.referrals.length - index}`, - }); + if (referral.card) { + this.linkedCards.push({ + ...referral.card, + displayName: `Referral #${this.planningReview.referrals.length - index}`, + }); + } } } } diff --git a/alcs-frontend/src/app/features/search/inquiry-search-table/inquiry-search-table.component.ts b/alcs-frontend/src/app/features/search/inquiry-search-table/inquiry-search-table.component.ts index d8a054aee8..3ef4122bd8 100644 --- a/alcs-frontend/src/app/features/search/inquiry-search-table/inquiry-search-table.component.ts +++ b/alcs-frontend/src/app/features/search/inquiry-search-table/inquiry-search-table.component.ts @@ -43,7 +43,7 @@ export class InquirySearchTableComponent { itemsPerPage = 20; total = 0; sortDirection: SortDirection = 'desc'; - sortField = 'fileId'; + sortField = 'dateSubmitted'; isLoading = false; constructor(private router: Router) {} diff --git a/alcs-frontend/src/app/features/search/planning-review-search-table/planning-review-search-table.component.html b/alcs-frontend/src/app/features/search/planning-review-search-table/planning-review-search-table.component.html index 8e329d8fa1..cce82b4163 100644 --- a/alcs-frontend/src/app/features/search/planning-review-search-table/planning-review-search-table.component.html +++ b/alcs-frontend/src/app/features/search/planning-review-search-table/planning-review-search-table.component.html @@ -16,6 +16,13 @@ + + Date Submitted to ALC + + {{ element.dateSubmitted | date | emptyColumn }} + + + Type diff --git a/alcs-frontend/src/app/features/search/planning-review-search-table/planning-review-search-table.component.ts b/alcs-frontend/src/app/features/search/planning-review-search-table/planning-review-search-table.component.ts index 9aeb07a46a..03b7cbb667 100644 --- a/alcs-frontend/src/app/features/search/planning-review-search-table/planning-review-search-table.component.ts +++ b/alcs-frontend/src/app/features/search/planning-review-search-table/planning-review-search-table.component.ts @@ -8,6 +8,7 @@ import { TableChange } from '../search.interface'; interface SearchResult { fileNumber: string; + dateSubmitted: number; applicant: string; localGovernmentName?: string; referenceId: string; @@ -36,13 +37,13 @@ export class PlanningReviewSearchTableComponent { @Output() tableChange = new EventEmitter(); - displayedColumns = ['fileId', 'type', 'applicant', 'government', 'status']; + displayedColumns = ['fileId', 'dateSubmitted', 'type', 'applicant', 'government', 'status']; dataSource: PlanningReviewSearchResultDto[] = []; itemsPerPage = 20; total = 0; sortDirection: SortDirection = 'desc'; - sortField = 'fileId'; + sortField = 'dateSubmitted'; isLoading = false; constructor(private router: Router) {} diff --git a/alcs-frontend/src/app/services/planning-review/planning-review.dto.ts b/alcs-frontend/src/app/services/planning-review/planning-review.dto.ts index 60cada333a..00616cc4ea 100644 --- a/alcs-frontend/src/app/services/planning-review/planning-review.dto.ts +++ b/alcs-frontend/src/app/services/planning-review/planning-review.dto.ts @@ -60,7 +60,7 @@ export interface PlanningReferralDto { responseDescription?: string; submissionDate: number; planningReview: PlanningReviewDto; - card: CardDto; + card?: CardDto; } export interface UpdatePlanningReviewDto { diff --git a/alcs-frontend/src/app/shared/header/search-bar/search-bar.component.ts b/alcs-frontend/src/app/shared/header/search-bar/search-bar.component.ts index d2c74f9367..513172ba6a 100644 --- a/alcs-frontend/src/app/shared/header/search-bar/search-bar.component.ts +++ b/alcs-frontend/src/app/shared/header/search-bar/search-bar.component.ts @@ -97,9 +97,7 @@ export class SearchBarComponent implements AfterViewInit { break; case 'COV': case 'PLAN': - await this.router.navigateByUrl( - `/board/${result.boardCode}?card=${result.referenceId}&type=${result.type}`, - ); + await this.router.navigate(['planning-review', result.referenceId]); break; default: this.toastService.showErrorToast(`Unable to navigate to ${result.referenceId}`); diff --git a/bin/migrate-oats-data/documents/post_launch/alcs_documents_to_planning_review_documents.py b/bin/migrate-oats-data/documents/post_launch/alcs_documents_to_planning_review_documents.py index 3c17808a21..54dbb39dff 100644 --- a/bin/migrate-oats-data/documents/post_launch/alcs_documents_to_planning_review_documents.py +++ b/bin/migrate-oats-data/documents/post_launch/alcs_documents_to_planning_review_documents.py @@ -119,7 +119,7 @@ def _map_data(row): "planning_review_uuid": row["planning_review_uuid"], "document_uuid": row["document_uuid"], "type_code": row["type_code"], - "visibility_flags": row["visibility_flags"], + "visibility_flags": '{""}', "oats_document_id": row["oats_document_id"], "oats_planning_review_id": row["oats_planning_review_id"], "audit_created_by": OATS_ETL_USER, diff --git a/bin/migrate-oats-data/documents/post_launch/sql/planning_review/alcs_documents_to_planning_review_documents.sql b/bin/migrate-oats-data/documents/post_launch/sql/planning_review/alcs_documents_to_planning_review_documents.sql index a9ebec6a29..4dfa1d8340 100644 --- a/bin/migrate-oats-data/documents/post_launch/sql/planning_review/alcs_documents_to_planning_review_documents.sql +++ b/bin/migrate-oats-data/documents/post_launch/sql/planning_review/alcs_documents_to_planning_review_documents.sql @@ -15,15 +15,6 @@ with oats_documents_to_map as ( select otm.planning_review_uuid, otm.document_uuid, otm.code as type_code, - ( - case - when is_public = 'Y' - and is_app_lg = 'Y' then '{P, A, C, G}'::text [] - when is_public = 'Y' then '{P}'::text [] - when is_app_lg = 'Y' then '{A, C, G}'::text [] - else '{}'::text [] - end - ) as visibility_flags, oats_document_id, oats_planning_review_id, otm."description" diff --git a/bin/migrate-oats-data/planning_review/decisions/init_planning_review_decisions.py b/bin/migrate-oats-data/planning_review/decisions/init_planning_review_decisions.py index a9bab23e14..6dba932cf4 100644 --- a/bin/migrate-oats-data/planning_review/decisions/init_planning_review_decisions.py +++ b/bin/migrate-oats-data/planning_review/decisions/init_planning_review_decisions.py @@ -5,6 +5,8 @@ add_timezone_and_keep_date_part, OatsToAlcsDecisionOutcomes, AlcsPlanningReviewOutcomes, + to_alcs_format, + get_now_with_offset, ) from db import inject_conn_pool from psycopg2.extras import RealDictCursor, execute_batch @@ -39,6 +41,8 @@ def process_planning_review_decisions(conn=None, batch_size=BATCH_UPLOAD_SIZE): failed_inserts_count = 0 successful_inserts_count = 0 last_planning_decision_id = 0 + last_file_number = 0 + last_decision_date = "" with open( "planning_review/sql/decisions/planning_review_decisions_insert.sql", @@ -47,10 +51,16 @@ def process_planning_review_decisions(conn=None, batch_size=BATCH_UPLOAD_SIZE): ) as sql_file: query = sql_file.read() while True: + query_condition = "" + if last_decision_date: + query_condition = f"WHERE pr.file_number::bigint >= {last_file_number} AND opd.decision_date >= '{last_decision_date}' AND opd.planning_decision_id > {last_planning_decision_id} ORDER BY pr.file_number::bigint, opd.decision_date, opd.planning_decision_id;" + else: + query_condition = f"WHERE pr.file_number::bigint >= {last_file_number} AND opd.planning_decision_id > {last_planning_decision_id} ORDER BY pr.file_number::bigint, opd.decision_date, opd.planning_decision_id;" + cursor.execute( f""" {query} - WHERE opd.planning_decision_id > {last_planning_decision_id} ORDER BY opd.planning_decision_id; + {query_condition} """ ) @@ -64,9 +74,18 @@ def process_planning_review_decisions(conn=None, batch_size=BATCH_UPLOAD_SIZE): successful_inserts_count = successful_inserts_count + len( inserted_data ) - last_planning_decision_id = dict(inserted_data[-1])[ - "planning_decision_id" - ] + last_record = dict(inserted_data[-1]) + last_planning_decision_id = last_record["planning_decision_id"] + last_inserted_row = next( + ( + row + for row in rows + if row["uuid"] == last_record["review_uuid"] + ), + None, + ) + last_file_number = last_inserted_row["file_number"] + last_decision_date = last_inserted_row["decision_date"] logger.debug( f"Retrieved/updated items count: {len(inserted_data)}; total successfully inserted planning referral so far {successful_inserts_count}; last updated planning_decision_id: {last_planning_decision_id}" @@ -106,7 +125,8 @@ def _insert_base_fields(conn, batch_size, cursor, rows): outcome_code, resolution_number, resolution_year, - date + date, + created_at ) VALUES ( %(review_uuid)s, @@ -116,13 +136,16 @@ def _insert_base_fields(conn, batch_size, cursor, rows): %(outcome_code)s, %(resolution_number)s, %(resolution_year)s, - %(date)s + %(date)s, + %(created_at)s ) + ON CONFLICT DO NOTHING; """ def _prepare_oats_planning_review_data(row_data_list): mapped_data_list = [] + insert_index = 0 for row in row_data_list: mapped_data_list.append( { @@ -134,8 +157,11 @@ def _prepare_oats_planning_review_data(row_data_list): "outcome_code": _map_outcome_code(row), "resolution_number": row["resolution_number"], "resolution_year": _map_resolution_year(row), + "created_at": to_alcs_format(get_now_with_offset(insert_index)), + "file_number": row["file_number"], } ) + insert_index += 1 return mapped_data_list diff --git a/bin/migrate-oats-data/planning_review/sql/decisions/planning_review_decisions_insert.sql b/bin/migrate-oats-data/planning_review/sql/decisions/planning_review_decisions_insert.sql index cbd40c3352..906ec68ada 100644 --- a/bin/migrate-oats-data/planning_review/sql/decisions/planning_review_decisions_insert.sql +++ b/bin/migrate-oats-data/planning_review/sql/decisions/planning_review_decisions_insert.sql @@ -5,7 +5,8 @@ SELECT opd.planning_acceptance_code, pr."uuid", opd.resolution_number, - opd.planning_decision_id + opd.planning_decision_id, + pr.file_number::bigint FROM oats.oats_planning_decisions opd JOIN alcs.planning_review pr ON opd.planning_review_id::TEXT = pr.file_number \ No newline at end of file diff --git a/portal-frontend/Dockerfile b/portal-frontend/Dockerfile index 8b3f351595..212720b0e3 100644 --- a/portal-frontend/Dockerfile +++ b/portal-frontend/Dockerfile @@ -13,6 +13,8 @@ RUN npm ci # Copy the source code to the /app directory COPY . . +ENV NODE_OPTIONS="--max-old-space-size=2048" + # Build the application RUN npm run build -- --output-path=dist --output-hashing=all diff --git a/portal-frontend/nginx.conf b/portal-frontend/nginx.conf index 8db9efc810..4f9a35f8a8 100644 --- a/portal-frontend/nginx.conf +++ b/portal-frontend/nginx.conf @@ -19,7 +19,7 @@ http { add_header 'X-XSS-Protection' '1; mode=block'; add_header 'Strict-Transport-Security' 'max-age=31536000; includeSubDomains; preload'; add_header 'Cache-control' 'no-cache'; - add_header 'Content-Security-Policy' "default-src 'self';img-src 'self';style-src 'unsafe-inline' 'self';connect-src $ENABLED_CONNECT_SRC; font-src 'self' https://fonts.gstatic.com https://fonts.googleapis.com; base-uri 'self'; object-src https://nrs.objectstore.gov.bc.ca; frame-src https://nrs.objectstore.gov.bc.ca;"; + add_header 'Content-Security-Policy' "default-src 'self'; img-src 'self'; style-src 'unsafe-inline' 'self'; connect-src $ENABLED_CONNECT_SRC https://spm.apps.gov.bc.ca; font-src 'self' https://fonts.gstatic.com https://fonts.googleapis.com; base-uri 'self'; object-src https://nrs.objectstore.gov.bc.ca; frame-src https://nrs.objectstore.gov.bc.ca; script-src 'self' https://www2.gov.bc.ca 'sha256-evje5KswYvntfuZqc5jmvUSANhIntI7Or6vVnjxGGQE=';"; add_header 'Permissions-Policy' 'camera=(), geolocation=(), microphone=()'; add_header 'Referrer-Policy' 'same-origin'; diff --git a/portal-frontend/src/app/app.component.ts b/portal-frontend/src/app/app.component.ts index a0fdf755fe..b3aa9e931c 100644 --- a/portal-frontend/src/app/app.component.ts +++ b/portal-frontend/src/app/app.component.ts @@ -17,6 +17,7 @@ export class AppComponent implements OnInit { this.router.events.subscribe((event) => { if (event instanceof NavigationEnd) { this.showHeaderFooter = !event.url.includes('/alcs/'); + (window as any).snowplow('trackPageView'); } }); } diff --git a/portal-frontend/src/assets/snowplow.js b/portal-frontend/src/assets/snowplow.js new file mode 100644 index 0000000000..d87b492873 --- /dev/null +++ b/portal-frontend/src/assets/snowplow.js @@ -0,0 +1,30 @@ +// +(function(p, l, o, w, i, n, g) { + if (!p[i]) { + p.GlobalSnowplowNamespace = p.GlobalSnowplowNamespace || []; + p.GlobalSnowplowNamespace.push(i); + p[i] = function() { + (p[i].q = p[i].q || []).push(arguments); + }; + p[i].q = p[i].q || []; + n = l.createElement(o); + g = l.getElementsByTagName(o)[0]; + n.async = 1; + n.src = w; + g.parentNode.insertBefore(n, g); + } +})(window, document, "script", "https://www2.gov.bc.ca/StaticWebResources/static/sp/sp-2-14-0.js", "snowplow"); +var collector = "spm.apps.gov.bc.ca"; +window.snowplow("newTracker", "rt", collector, { + appId: "Snowplow_standalone", + cookieLifetime: 86400 * 548, + platform: "web", + post: true, + forceSecureTracker: true, + contexts: { + webPage: true, performanceTiming: true + } +}); +window.snowplow("enableActivityTracking", 30, 30); // Ping every 30 seconds after 30 seconds +window.snowplow("enableLinkClickTracking"); +// diff --git a/portal-frontend/src/index.html b/portal-frontend/src/index.html index ca34a10771..8f28f22c83 100644 --- a/portal-frontend/src/index.html +++ b/portal-frontend/src/index.html @@ -9,41 +9,7 @@ - - + diff --git a/services/apps/alcs/src/alcs/meetings/decision-meeting.controller.ts b/services/apps/alcs/src/alcs/meetings/decision-meeting.controller.ts index e88280287f..32daf27aaf 100644 --- a/services/apps/alcs/src/alcs/meetings/decision-meeting.controller.ts +++ b/services/apps/alcs/src/alcs/meetings/decision-meeting.controller.ts @@ -192,7 +192,7 @@ export class DecisionMeetingController { (meeting) => meeting.uuid === planningReferral.planningReview.uuid, ); - if (!meetingDate) { + if (!meetingDate || !planningReferral.card) { return []; } @@ -201,10 +201,10 @@ export class DecisionMeetingController { meetingDate: new Date(meetingDate.next_meeting).getTime(), fileNumber: planningReferral.planningReview.fileNumber, applicant: planningReferral.planningReview.documentName, - boardCode: planningReferral.card!.board.code, + boardCode: planningReferral.card.board.code, type: CARD_TYPE.PLAN, assignee: this.mapper.map( - planningReferral.card!.assignee, + planningReferral.card.assignee, User, UserDto, ), diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service.ts index 87e7c14355..f523b6d3de 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service.ts @@ -166,7 +166,6 @@ export class NoticeOfIntentSubmissionStatusService { async saveSubmissionToSubmissionStatus( submissionStatus: NoticeOfIntentSubmissionToSubmissionStatus, ) { - console.log('save', submissionStatus); return await this.statusesRepository.save(submissionStatus); } } diff --git a/services/apps/alcs/src/alcs/notification/notification.controller.ts b/services/apps/alcs/src/alcs/notification/notification.controller.ts index 016b6b9536..ed7b14ca28 100644 --- a/services/apps/alcs/src/alcs/notification/notification.controller.ts +++ b/services/apps/alcs/src/alcs/notification/notification.controller.ts @@ -1,4 +1,7 @@ -import { ServiceNotFoundException } from '@app/common/exceptions/base.exception'; +import { + BaseServiceException, + ServiceNotFoundException, +} from '@app/common/exceptions/base.exception'; import { Body, Controller, @@ -120,11 +123,18 @@ export class NotificationController { ); if (document) { - await this.notificationSubmissionService.sendAndRecordLTSAPackage( - submission, - document, - user, - ); + const emailDidSend = + await this.notificationSubmissionService.sendAndRecordLTSAPackage( + submission, + document, + user, + ); + + if (!emailDidSend) { + throw new BaseServiceException( + `Failed to send LTSA Package ${fileNumber}`, + ); + } } else { throw new ServiceNotFoundException( `Failed to find LTSA Letter on File Number ${fileNumber}`, diff --git a/services/apps/alcs/src/alcs/planning-review/planning-review-decision/planning-review-decision.controller.ts b/services/apps/alcs/src/alcs/planning-review/planning-review-decision/planning-review-decision.controller.ts index 7df9104110..0a24379981 100644 --- a/services/apps/alcs/src/alcs/planning-review/planning-review-decision/planning-review-decision.controller.ts +++ b/services/apps/alcs/src/alcs/planning-review/planning-review-decision/planning-review-decision.controller.ts @@ -32,7 +32,7 @@ import { PlanningReviewDecisionService } from './planning-review-decision.servic @UseGuards(RolesGuard) export class PlanningReviewDecisionController { constructor( - private plannigReviewDecisionService: PlanningReviewDecisionService, + private planningReviewDecisionService: PlanningReviewDecisionService, @InjectMapper() private mapper: Mapper, ) {} @@ -42,7 +42,7 @@ export class PlanningReviewDecisionController { @Param('fileNumber') fileNumber, ): Promise { const decisions = - await this.plannigReviewDecisionService.getByFileNumber(fileNumber); + await this.planningReviewDecisionService.getByFileNumber(fileNumber); return await this.mapper.mapArrayAsync( decisions, @@ -54,7 +54,7 @@ export class PlanningReviewDecisionController { @Get('/codes') @UserRoles(...ANY_AUTH_ROLE) async getCodes() { - const codes = await this.plannigReviewDecisionService.fetchCodes(); + const codes = await this.planningReviewDecisionService.fetchCodes(); return await this.mapper.mapArrayAsync( codes.outcomes, PlanningReviewDecisionOutcomeCode, @@ -65,7 +65,7 @@ export class PlanningReviewDecisionController { @Get('/:uuid') @UserRoles(...ANY_AUTH_ROLE) async get(@Param('uuid') uuid: string): Promise { - const decision = await this.plannigReviewDecisionService.get(uuid); + const decision = await this.planningReviewDecisionService.get(uuid); return this.mapper.mapAsync( decision, @@ -80,7 +80,7 @@ export class PlanningReviewDecisionController { @Body() createDto: CreatePlanningReviewDecisionDto, ): Promise { const newDecision = - await this.plannigReviewDecisionService.create(createDto); + await this.planningReviewDecisionService.create(createDto); return this.mapper.mapAsync( newDecision, @@ -95,7 +95,7 @@ export class PlanningReviewDecisionController { @Param('uuid') uuid: string, @Body() updateDto: UpdatePlanningReviewDecisionDto, ): Promise { - const updatedDecision = await this.plannigReviewDecisionService.update( + const updatedDecision = await this.planningReviewDecisionService.update( uuid, updateDto, ); @@ -110,7 +110,7 @@ export class PlanningReviewDecisionController { @Delete('/:uuid') @UserRoles(...ANY_AUTH_ROLE) async delete(@Param('uuid') uuid: string) { - return await this.plannigReviewDecisionService.delete(uuid); + return await this.planningReviewDecisionService.delete(uuid); } @Post('/:uuid/file') @@ -121,7 +121,7 @@ export class PlanningReviewDecisionController { } const file = req.body.file; - await this.plannigReviewDecisionService.attachDocument( + await this.planningReviewDecisionService.attachDocument( decisionUuid, file, req.user.entity, @@ -138,7 +138,7 @@ export class PlanningReviewDecisionController { @Param('documentUuid') documentUuid: string, @Body() body: { fileName: string }, ) { - await this.plannigReviewDecisionService.updateDocument( + await this.planningReviewDecisionService.updateDocument( documentUuid, body.fileName, ); @@ -154,7 +154,7 @@ export class PlanningReviewDecisionController { @Param('fileUuid') documentUuid: string, ) { const downloadUrl = - await this.plannigReviewDecisionService.getDownloadUrl(documentUuid); + await this.planningReviewDecisionService.getDownloadUrl(documentUuid); return { url: downloadUrl, }; @@ -166,7 +166,7 @@ export class PlanningReviewDecisionController { @Param('uuid') decisionUuid: string, @Param('fileUuid') documentUuid: string, ) { - const downloadUrl = await this.plannigReviewDecisionService.getDownloadUrl( + const downloadUrl = await this.planningReviewDecisionService.getDownloadUrl( documentUuid, true, ); @@ -181,7 +181,7 @@ export class PlanningReviewDecisionController { @Param('uuid') decisionUuid: string, @Param('fileUuid') documentUuid: string, ) { - await this.plannigReviewDecisionService.deleteDocument(documentUuid); + await this.planningReviewDecisionService.deleteDocument(documentUuid); return {}; } @@ -190,7 +190,7 @@ export class PlanningReviewDecisionController { async getNextAvailableResolutionNumber( @Param('resolutionYear') resolutionYear: number, ) { - return this.plannigReviewDecisionService.generateResolutionNumber( + return this.planningReviewDecisionService.generateResolutionNumber( resolutionYear, ); } diff --git a/services/apps/alcs/src/alcs/planning-review/planning-review-decision/planning-review-decision.service.ts b/services/apps/alcs/src/alcs/planning-review/planning-review-decision/planning-review-decision.service.ts index c9c12901f3..0e0dac8c1e 100644 --- a/services/apps/alcs/src/alcs/planning-review/planning-review-decision/planning-review-decision.service.ts +++ b/services/apps/alcs/src/alcs/planning-review/planning-review-decision/planning-review-decision.service.ts @@ -5,7 +5,7 @@ import { import { MultipartFile } from '@fastify/multipart'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { In, IsNull, Repository } from 'typeorm'; +import { IsNull, Repository } from 'typeorm'; import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM, diff --git a/services/apps/alcs/src/alcs/search/search.controller.ts b/services/apps/alcs/src/alcs/search/search.controller.ts index f4472d5949..600c99d0fd 100644 --- a/services/apps/alcs/src/alcs/search/search.controller.ts +++ b/services/apps/alcs/src/alcs/search/search.controller.ts @@ -581,6 +581,7 @@ export class SearchController { documentName: planningReview.documentName, referenceId: planningReview.fileNumber, fileNumber: planningReview.fileNumber, + dateSubmitted: planningReview.dateSubmittedToAlc, open: planningReview.open, type: { code: planningReview.planningReviewType_code, diff --git a/services/apps/alcs/src/alcs/search/search.dto.ts b/services/apps/alcs/src/alcs/search/search.dto.ts index c4b714c64b..b37361d719 100644 --- a/services/apps/alcs/src/alcs/search/search.dto.ts +++ b/services/apps/alcs/src/alcs/search/search.dto.ts @@ -56,6 +56,7 @@ export class PlanningReviewSearchResultDto { documentName: string | null; referenceId: string | null; localGovernmentName: string | null; + dateSubmitted: number; fileNumber: string; class: SearchEntityClass; open: boolean; diff --git a/services/apps/alcs/src/main.module.ts b/services/apps/alcs/src/main.module.ts index 1349193ce5..a64db47502 100644 --- a/services/apps/alcs/src/main.module.ts +++ b/services/apps/alcs/src/main.module.ts @@ -38,7 +38,6 @@ import { UserModule } from './user/user.module'; }), ClsModule.forRoot({ global: true, - middleware: { mount: true }, }), WinstonModule, DocumentModule, diff --git a/services/apps/alcs/src/main.ts b/services/apps/alcs/src/main.ts index 1079e75b4e..e30e641492 100644 --- a/services/apps/alcs/src/main.ts +++ b/services/apps/alcs/src/main.ts @@ -9,6 +9,7 @@ import { } from '@nestjs/platform-fastify'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import * as config from 'config'; +import { ClsMiddleware } from 'nestjs-cls'; import { S3StreamLogger } from 's3-streamlogger'; import { install } from 'source-map-support'; import * as winston from 'winston'; @@ -148,7 +149,6 @@ function setupLogger() { http: 4, debug: 5, verbose: 6, - log: 7, }, transports: config.get('ENV') === 'production' @@ -170,6 +170,7 @@ async function bootstrap() { }, ); app.useLogger(logger); + app.use(new ClsMiddleware().use); const extraArg = process.argv[2]; if (extraArg === 'graph') { diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts index f493844a05..80a6465209 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts @@ -610,56 +610,6 @@ describe('ApplicationSubmissionController', () => { }); }); - it('should only send status email for first time non-TUR applications', async () => { - const mockFileId = 'file-id'; - mockAppSubmissionService.submitToLg.mockResolvedValue( - new ApplicationSubmissionToSubmissionStatus(), - ); - const mockOwner = new ApplicationOwner({ uuid: primaryContactOwnerUuid }); - const mockApplicationSubmission = new ApplicationSubmission({ - typeCode: 'NOT-TURP', - owners: [mockOwner], - primaryContactOwnerUuid, - localGovernmentUuid, - submissionStatuses: [ - new ApplicationSubmissionToSubmissionStatus({ - statusTypeCode: SUBMISSION_STATUS.INCOMPLETE, - submissionUuid: 'fake', - effectiveDate: new Date(), - }), - ], - }); - mockAppSubmissionService.verifyAccessByUuid.mockResolvedValue( - mockApplicationSubmission, - ); - mockAppValidationService.validateSubmission.mockResolvedValue({ - submission: mockApplicationSubmission as ValidatedApplicationSubmission, - errors: [], - }); - mockStatusEmailService.sendApplicationStatusEmail.mockResolvedValue(); - - const mockGovernment = new LocalGovernment({ uuid: localGovernmentUuid }); - mockStatusEmailService.getApplicationEmailData.mockResolvedValue({ - applicationSubmission: mockApplicationSubmission, - primaryContact: mockOwner, - submissionGovernment: mockGovernment, - }); - - await controller.submitAsApplicant(mockFileId, { - user: { - entity: new User(), - }, - }); - - expect(mockAppSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( - 1, - ); - expect(mockAppSubmissionService.submitToLg).toHaveBeenCalledTimes(1); - expect( - mockStatusEmailService.sendApplicationStatusEmail, - ).toHaveBeenCalledTimes(0); - }); - it('should throw an exception if application fails validation', async () => { const mockFileId = 'file-id'; const mockApplicationSubmission = new ApplicationSubmission({ diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts b/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts index dc317ddb00..ecd7d0a4b4 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts @@ -16,8 +16,8 @@ import { } from '../../../../../libs/common/src/exceptions/base.exception'; import { generateCANCApplicationHtml } from '../../../../../templates/emails/cancelled'; import { - generateSUBGTurApplicantHtml, generateSUBGNoReviewGovernmentTemplateEmail, + generateSUBGTurApplicantHtml, } from '../../../../../templates/emails/submitted-to-alc'; import { generateSUBGCoveApplicantHtml } from '../../../../../templates/emails/submitted-to-alc/cove-applicant.template'; import { @@ -288,39 +288,25 @@ export class ApplicationSubmissionController { ); if (matchingType.requiresGovernmentReview) { - const wasSubmittedToLfng = validatedSubmission.submissionStatuses.find( - (s) => - [ - SUBMISSION_STATUS.SUBMITTED_TO_LG, - SUBMISSION_STATUS.IN_REVIEW_BY_LG, - SUBMISSION_STATUS.WRONG_GOV, - SUBMISSION_STATUS.INCOMPLETE, - ].includes(s.statusTypeCode as SUBMISSION_STATUS) && - !!s.effectiveDate, - ); - - // Send status emails for first time submissions - if (!wasSubmittedToLfng) { - if (primaryContact) { - await this.statusEmailService.sendApplicationStatusEmail({ - generateStatusHtml: generateSUBGApplicantHtml, - status: SUBMISSION_STATUS.SUBMITTED_TO_LG, - applicationSubmission: validatedSubmission, - government: submissionGovernment, - parentType: PARENT_TYPE.APPLICATION, - primaryContact, - }); - } + if (primaryContact) { + await this.statusEmailService.sendApplicationStatusEmail({ + generateStatusHtml: generateSUBGApplicantHtml, + status: SUBMISSION_STATUS.SUBMITTED_TO_LG, + applicationSubmission: validatedSubmission, + government: submissionGovernment, + parentType: PARENT_TYPE.APPLICATION, + primaryContact, + }); + } - if (submissionGovernment) { - await this.statusEmailService.sendApplicationStatusEmail({ - generateStatusHtml: generateSUBGGovernmentHtml, - status: SUBMISSION_STATUS.SUBMITTED_TO_LG, - applicationSubmission: validatedSubmission, - government: submissionGovernment, - parentType: PARENT_TYPE.APPLICATION, - }); - } + if (submissionGovernment) { + await this.statusEmailService.sendApplicationStatusEmail({ + generateStatusHtml: generateSUBGGovernmentHtml, + status: SUBMISSION_STATUS.SUBMITTED_TO_LG, + applicationSubmission: validatedSubmission, + government: submissionGovernment, + parentType: PARENT_TYPE.APPLICATION, + }); } return await this.applicationSubmissionService.submitToLg( diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.spec.ts index a76135884e..1c2a6e49cb 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.spec.ts @@ -228,7 +228,7 @@ describe('NoticeOfIntentSubmissionDraftService', () => { mockGenerateSubmissionDocumentService.generateUpdate.mockResolvedValue(); mockNoiSubmissionStatusService.removeStatuses.mockResolvedValue({} as any); mockNoiService.updateNoticeOfIntentInfo.mockResolvedValue(); - mockLocalGovernmentService.getByGuid.mockResolvedValue( + mockLocalGovernmentService.getByUuid.mockResolvedValue( new LocalGovernment({ uuid: 'new' }), ); mockStatusEmailService.sendNoticeOfIntentStatusEmail.mockResolvedValue(); @@ -248,8 +248,8 @@ describe('NoticeOfIntentSubmissionDraftService', () => { mockGenerateSubmissionDocumentService.generateUpdate, ).toHaveBeenCalledTimes(1); expect(mockNoiService.updateNoticeOfIntentInfo).toHaveBeenCalledTimes(1); - expect(mockLocalGovernmentService.getByGuid).toHaveBeenCalledTimes(1); - expect(mockLocalGovernmentService.getByGuid).toHaveBeenCalledWith('new'); + expect(mockLocalGovernmentService.getByUuid).toHaveBeenCalledTimes(1); + expect(mockLocalGovernmentService.getByUuid).toHaveBeenCalledWith('new'); expect( mockStatusEmailService.sendNoticeOfIntentStatusEmail, ).toHaveBeenCalledTimes(1); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.ts index c953e175fd..53ddf6feb5 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.ts @@ -244,7 +244,7 @@ export class NoticeOfIntentSubmissionDraftService { } private async sendGovernmentEmail(savedDraft: NoticeOfIntentSubmission) { - const submissionGovernment = await this.localGovernmentService.getByGuid( + const submissionGovernment = await this.localGovernmentService.getByUuid( savedDraft.localGovernmentUuid!, ); @@ -267,7 +267,7 @@ export class NoticeOfIntentSubmissionDraftService { ); } catch (error) { this.logger.error( - `Error generating NOI submission document {error}`, + `Error generating NOI submission document ${error}`, error, ); } diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts index e69108c141..72c6777912 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts @@ -406,7 +406,7 @@ export class NotificationSubmissionService { submission: NotificationSubmission, document: NotificationDocument, user: User, - ) { + ): Promise { const templateData = await this.generateSrwEmailData(submission, document); const didSend = await this.emailService.sendEmail({ @@ -442,6 +442,8 @@ export class NotificationSubmissionService { NOTIFICATION_STATUS.ALC_RESPONSE_SENT, ); } + + return didSend; } private async generateSrwEmailData( @@ -470,7 +472,7 @@ export class NotificationSubmissionService { ); if (localGovernment && localGovernment.emails) { - ccEmails = localGovernment.emails; + ccEmails = localGovernment.emails.filter((email) => email !== ''); } } diff --git a/services/apps/alcs/src/providers/email/email.service.ts b/services/apps/alcs/src/providers/email/email.service.ts index eccac508bd..5a0394348d 100644 --- a/services/apps/alcs/src/providers/email/email.service.ts +++ b/services/apps/alcs/src/providers/email/email.service.ts @@ -102,7 +102,7 @@ export class EmailService { parentId?: string; triggerStatus?: string; attachments?: Document[]; - }) { + }): Promise { const serviceUrl = this.config.get('CHES.URL'); const from = this.config.get('CHES.FROM'); const token = await this.getToken(); @@ -134,7 +134,7 @@ export class EmailService { { to, body, subject, cc, bcc }, 'EmailService did not send the email. Set CHES.MODE to production if you need to send an email.', ); - return; + return false; } const res = await firstValueFrom( this.httpService.post<{ diff --git a/services/config/default.json b/services/config/default.json index f4b411c42c..6cfa5e4351 100644 --- a/services/config/default.json +++ b/services/config/default.json @@ -32,7 +32,9 @@ "AUTH_SERVER": "test.loginproxy.gov.bc.ca", "AUTH_SERVER_URL": "https://test.loginproxy.gov.bc.ca/auth", "AUTH_TOKEN_URL": "https://test.loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/token", - "SCOPES": ["openid"], + "SCOPES": [ + "openid" + ], "REALM": "standard" }, "SITEMINDER": { @@ -61,12 +63,14 @@ "MAX_FILE_SIZE": 104857600 }, "REDIS": { - "HOST": "localhost", + "HOST": "redis", "PORT": "6379", - "PASSWORD": "" + "PASSWORD": "redis" }, "EMAIL": { - "DEFAULT_ADMINS": [""] + "DEFAULT_ADMINS": [ + "" + ] }, "GRPC": { "BIND_URL": "localhost:50057",