diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html
index bfbf87656a..ce40b43dbd 100644
--- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html
+++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html
@@ -83,8 +83,24 @@
Res #{{ decision.resolutionNumber }}/{{ decision.resolutionYear }}
+
+
+
+
+
-
+
Res #{{ decision.resolutionNumber }}/{{ decision.resolutionYear }}
+
+
+
+
+
+
+ Flagged for condition follow-up because: {{ decision.reasonFlagged }}
+
+
+
+
+
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss
index 908ed1c11c..9cd8b4d08b 100644
--- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss
+++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss
@@ -53,8 +53,11 @@ hr {
}
.header {
- display: flex;
- justify-content: space-between;
+ display: grid;
+ grid-template-columns: auto auto auto;
+ grid-template-rows: auto auto;
+ row-gap: 10px;
+ column-gap: 28px;
margin-bottom: 36px;
.title {
@@ -62,6 +65,8 @@ hr {
align-items: center;
justify-content: space-between;
gap: 28px;
+ grid-row: 1/2;
+ grid-column: 1/2;
.days {
display: inline-block;
@@ -78,6 +83,11 @@ hr {
}
}
+.edit-decision-button {
+ grid-row: 1/2;
+ grid-column: 3/4;
+}
+
.loading-overlay {
position: absolute;
z-index: 2;
@@ -187,3 +197,85 @@ hr {
color: colors.$link-color;
}
}
+
+.flag-button-container {
+ justify-self: end;
+ grid-row: 2/3;
+ grid-column: 1/4;
+
+ @media screen and (min-width: 1440px) {
+ grid-row: 1/2;
+ grid-column: 2/3;
+ }
+}
+
+.flag-button {
+ display: flex;
+ align-items: center;
+ column-gap: 5px;
+
+ text-align: left;
+ font-weight: normal;
+ text-wrap: nowrap;
+
+ background-color: transparent;
+ padding: 5px;
+ border: none;
+ border-radius: 5px;
+ margin: 0;
+
+ &:hover {
+ background-color: colors.$grey-light;
+ }
+
+ mat-icon {
+ flex-shrink: 0;
+ }
+
+ &.flagged {
+ mat-icon {
+ color: blue;
+ }
+ }
+}
+
+.flag-details {
+ background-color: white;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ padding: 16px;
+ border: 1px solid colors.$grey;
+ border-radius: 4px;
+}
+
+.flag-details-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.flag-details-flagger {
+ display: flex;
+ gap: 5px;
+
+ mat-icon {
+ color: blue;
+ }
+}
+
+.flag-details-body {
+ line-height: 1.5;
+}
+
+.flag-details-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+}
+
+.flag-details-edited-details {
+ font-size: 12px;
+ color: colors.$grey;
+}
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.spec.ts
index a1b490e13f..8b28e9a38b 100644
--- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.spec.ts
+++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.spec.ts
@@ -16,8 +16,8 @@ import {
import { ApplicationDecisionV2Service } from '../../../../services/application/decision/application-decision-v2/application-decision-v2.service';
import { ToastService } from '../../../../services/toast/toast.service';
import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service';
-
import { DecisionV2Component } from './decision-v2.component';
+import { HttpClient } from '@angular/common/http';
describe('DecisionV2Component', () => {
let component: DecisionV2Component;
@@ -25,6 +25,7 @@ describe('DecisionV2Component', () => {
let mockApplicationDecisionService: DeepMocked;
let mockAppDetailService: DeepMocked;
let mockApplicationDecisionComponentService: DeepMocked;
+ let mockHttpClient: DeepMocked;
beforeEach(async () => {
mockApplicationDecisionService = createMock();
@@ -36,6 +37,8 @@ describe('DecisionV2Component', () => {
mockApplicationDecisionComponentService = createMock();
+ mockHttpClient = createMock();
+
await TestBed.configureTestingModule({
imports: [MatSnackBarModule, MatMenuModule],
declarations: [DecisionV2Component],
@@ -72,6 +75,10 @@ describe('DecisionV2Component', () => {
provide: ActivatedRoute,
useValue: {},
},
+ {
+ provide: HttpClient,
+ useValue: mockHttpClient,
+ },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts
index ff54b69469..c0f895a5ef 100644
--- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts
+++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts
@@ -31,6 +31,11 @@ import { formatDateForApi } from '../../../../shared/utils/api-date-formatter';
import { RevertToDraftDialogComponent } from './revert-to-draft-dialog/revert-to-draft-dialog.component';
import { ApplicationConditionWithStatus, getEndDate } from '../../../../shared/utils/decision-methods';
import { openFileInline } from '../../../../shared/utils/file';
+import { UserService } from '../../../../services/user/user.service';
+import { UserDto } from '../../../../services/user/user.dto';
+import { FlagDialogComponent, FlagDialogIO } from '../../../../shared/flag-dialog/flag-dialog.component';
+import { UpdateApplicationDecisionDto } from '../../../../services/application/decision/application-decision-v2/application-decision-v2.dto';
+import moment from 'moment';
type LoadingDecision = ApplicationDecisionWithLinkedResolutionDto & {
loading: boolean;
@@ -67,6 +72,7 @@ export class DecisionV2Component implements OnInit, OnDestroy {
isSummary = false;
conditions: Record = {};
+ profile: UserDto | undefined;
constructor(
public dialog: MatDialog,
@@ -78,6 +84,7 @@ export class DecisionV2Component implements OnInit, OnDestroy {
private router: Router,
private activatedRouter: ActivatedRoute,
private elementRef: ElementRef,
+ private userService: UserService,
) {}
ngOnInit(): void {
@@ -90,6 +97,10 @@ export class DecisionV2Component implements OnInit, OnDestroy {
this.application = application;
}
});
+
+ this.userService.$userProfile.pipe(takeUntil(this.$destroy)).subscribe((profile) => {
+ this.profile = profile;
+ });
}
async loadDecisions(fileNumber: string) {
@@ -324,4 +335,81 @@ export class DecisionV2Component implements OnInit, OnDestroy {
return DECISION_CONDITION_ONGOING_LABEL;
}
}
+
+ async flag(decision: ApplicationDecisionWithLinkedResolutionDto, isEditing: boolean) {
+ this.dialog
+ .open(FlagDialogComponent, {
+ minWidth: '800px',
+ maxWidth: '800px',
+ maxHeight: '80vh',
+ width: '90%',
+ autoFocus: false,
+ data: {
+ isEditing,
+ decisionNumber: decision.index,
+ reasonFlagged: decision.reasonFlagged,
+ followUpAt: decision.followUpAt,
+ },
+ })
+ .beforeClosed()
+ .subscribe(async ({ isEditing, reasonFlagged, followUpAt, isSaving }: FlagDialogIO) => {
+ if (isSaving) {
+ const updateDto: UpdateApplicationDecisionDto = {
+ isDraft: decision.isDraft,
+ isFlagged: true,
+ reasonFlagged,
+ flagEditedByUuid: this.profile?.uuid,
+ flagEditedAt: moment().toDate().getTime(),
+ };
+
+ if (!isEditing) {
+ updateDto.flaggedByUuid = this.profile?.uuid;
+ }
+
+ if (followUpAt !== undefined) {
+ updateDto.followUpAt = followUpAt;
+ }
+
+ await this.decisionService.update(decision.uuid, updateDto);
+ await this.loadDecisions(this.fileNumber);
+ }
+ });
+ }
+
+ async unflag(decision: ApplicationDecisionWithLinkedResolutionDto) {
+ this.confirmationDialogService
+ .openDialog({
+ title: `Unflag Decision #${decision.index}`,
+ body: `Warning: Only remove if flagged in error.
+
+
+ This action will also remove the follow-up date and explanatory text
+ associated with the flag and cannot be undone.
+
+
+ Are you sure you want to remove the flag?`,
+ })
+ .subscribe(async (confirmed) => {
+ if (confirmed) {
+ await this.decisionService.update(decision.uuid, {
+ isDraft: decision.isDraft,
+ isFlagged: false,
+ reasonFlagged: null,
+ followUpAt: null,
+ flaggedByUuid: null,
+ flagEditedByUuid: null,
+ flagEditedAt: null,
+ });
+ await this.loadDecisions(this.fileNumber);
+ }
+ });
+ }
+
+ formatDate(timestamp?: number | null, includeTime = false): string {
+ if (timestamp === undefined || timestamp === null) {
+ return '';
+ }
+
+ return moment(new Date(timestamp)).format(`YYYY-MMM-DD ${includeTime ? 'hh:mm:ss A' : ''}`);
+ }
}
diff --git a/alcs-frontend/src/app/features/board/board.component.ts b/alcs-frontend/src/app/features/board/board.component.ts
index be9da77a25..2d6914a78a 100644
--- a/alcs-frontend/src/app/features/board/board.component.ts
+++ b/alcs-frontend/src/app/features/board/board.component.ts
@@ -537,6 +537,7 @@ export class BoardComponent implements OnInit, OnDestroy {
dateReceived: 0,
isExpired,
isPastDue,
+ decisionIsFlagged: applicationDecisionCondition.decisionIsFlagged,
};
}
diff --git a/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.html
index 5325e15bf0..027d393d6b 100644
--- a/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.html
+++ b/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.html
@@ -34,6 +34,8 @@
[type]="getStatusPill('RECONSIDERATION')"
>
+
+
+
+
+
+
+
+
+ Flagged for condition follow-up because: {{ decision.reasonFlagged }}
+
+
+
+
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss
index 908ed1c11c..9cd8b4d08b 100644
--- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss
+++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss
@@ -53,8 +53,11 @@ hr {
}
.header {
- display: flex;
- justify-content: space-between;
+ display: grid;
+ grid-template-columns: auto auto auto;
+ grid-template-rows: auto auto;
+ row-gap: 10px;
+ column-gap: 28px;
margin-bottom: 36px;
.title {
@@ -62,6 +65,8 @@ hr {
align-items: center;
justify-content: space-between;
gap: 28px;
+ grid-row: 1/2;
+ grid-column: 1/2;
.days {
display: inline-block;
@@ -78,6 +83,11 @@ hr {
}
}
+.edit-decision-button {
+ grid-row: 1/2;
+ grid-column: 3/4;
+}
+
.loading-overlay {
position: absolute;
z-index: 2;
@@ -187,3 +197,85 @@ hr {
color: colors.$link-color;
}
}
+
+.flag-button-container {
+ justify-self: end;
+ grid-row: 2/3;
+ grid-column: 1/4;
+
+ @media screen and (min-width: 1440px) {
+ grid-row: 1/2;
+ grid-column: 2/3;
+ }
+}
+
+.flag-button {
+ display: flex;
+ align-items: center;
+ column-gap: 5px;
+
+ text-align: left;
+ font-weight: normal;
+ text-wrap: nowrap;
+
+ background-color: transparent;
+ padding: 5px;
+ border: none;
+ border-radius: 5px;
+ margin: 0;
+
+ &:hover {
+ background-color: colors.$grey-light;
+ }
+
+ mat-icon {
+ flex-shrink: 0;
+ }
+
+ &.flagged {
+ mat-icon {
+ color: blue;
+ }
+ }
+}
+
+.flag-details {
+ background-color: white;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ padding: 16px;
+ border: 1px solid colors.$grey;
+ border-radius: 4px;
+}
+
+.flag-details-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.flag-details-flagger {
+ display: flex;
+ gap: 5px;
+
+ mat-icon {
+ color: blue;
+ }
+}
+
+.flag-details-body {
+ line-height: 1.5;
+}
+
+.flag-details-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+}
+
+.flag-details-edited-details {
+ font-size: 12px;
+ color: colors.$grey;
+}
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.spec.ts
index 5ea1e1d7dc..c7134f5ddf 100644
--- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.spec.ts
+++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.spec.ts
@@ -16,6 +16,7 @@ import { NoticeOfIntentDetailService } from '../../../../services/notice-of-inte
import { NoticeOfIntentDto } from '../../../../services/notice-of-intent/notice-of-intent.dto';
import { ToastService } from '../../../../services/toast/toast.service';
import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service';
+import { HttpClient } from '@angular/common/http';
import { DecisionV2Component } from './decision-v2.component';
@@ -25,6 +26,7 @@ describe('DecisionV2Component', () => {
let mockNOIDecisionService: DeepMocked;
let mockNOIDetailService: DeepMocked;
let mockNOIDecisionComponentService: DeepMocked;
+ let mockHttpClient: DeepMocked;
beforeEach(async () => {
mockNOIDecisionService = createMock();
@@ -36,6 +38,8 @@ describe('DecisionV2Component', () => {
mockNOIDecisionComponentService = createMock();
+ mockHttpClient = createMock();
+
await TestBed.configureTestingModule({
imports: [MatSnackBarModule, MatMenuModule],
declarations: [DecisionV2Component],
@@ -72,6 +76,10 @@ describe('DecisionV2Component', () => {
provide: ActivatedRoute,
useValue: {},
},
+ {
+ provide: HttpClient,
+ useValue: mockHttpClient,
+ },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.ts
index 735e301331..7d91b6456e 100644
--- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.ts
+++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.ts
@@ -8,6 +8,7 @@ import {
NOI_DECISION_COMPONENT_TYPE,
NoticeOfIntentDecisionDto,
NoticeOfIntentDecisionOutcomeCodeDto,
+ UpdateNoticeOfIntentDecisionDto,
} from '../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision.dto';
import { NoticeOfIntentDetailService } from '../../../../services/notice-of-intent/notice-of-intent-detail.service';
import { NoticeOfIntentDto } from '../../../../services/notice-of-intent/notice-of-intent.dto';
@@ -26,6 +27,10 @@ import { ConfirmationDialogService } from '../../../../shared/confirmation-dialo
import { formatDateForApi } from '../../../../shared/utils/api-date-formatter';
import { RevertToDraftDialogComponent } from './revert-to-draft-dialog/revert-to-draft-dialog.component';
import { NoticeOfIntentConditionWithStatus, getEndDate } from '../../../../shared/utils/decision-methods';
+import moment from 'moment';
+import { FlagDialogComponent, FlagDialogIO } from '../../../../shared/flag-dialog/flag-dialog.component';
+import { UserDto } from '../../../../services/user/user.dto';
+import { UserService } from '../../../../services/user/user.service';
type LoadingDecision = NoticeOfIntentDecisionDto & {
loading: boolean;
@@ -58,8 +63,9 @@ export class DecisionV2Component implements OnInit, OnDestroy {
COMPONENT_TYPE = NOI_DECISION_COMPONENT_TYPE;
isSummary = false;
-
+
conditions: Record = {};
+ profile: UserDto | undefined;
constructor(
public dialog: MatDialog,
@@ -71,6 +77,7 @@ export class DecisionV2Component implements OnInit, OnDestroy {
private router: Router,
private activatedRouter: ActivatedRoute,
private elementRef: ElementRef,
+ private userService: UserService,
) {}
ngOnInit(): void {
@@ -83,6 +90,10 @@ export class DecisionV2Component implements OnInit, OnDestroy {
this.noticeOfIntent = noticeOfIntent;
}
});
+
+ this.userService.$userProfile.pipe(takeUntil(this.$destroy)).subscribe((profile) => {
+ this.profile = profile;
+ });
}
async loadDecisions(fileNumber: string) {
@@ -279,4 +290,81 @@ export class DecisionV2Component implements OnInit, OnDestroy {
return DECISION_CONDITION_ONGOING_LABEL;
}
}
+
+ async flag(decision: LoadingDecision, index: number, isEditing: boolean) {
+ this.dialog
+ .open(FlagDialogComponent, {
+ minWidth: '800px',
+ maxWidth: '800px',
+ maxHeight: '80vh',
+ width: '90%',
+ autoFocus: false,
+ data: {
+ isEditing,
+ decisionNumber: index,
+ reasonFlagged: decision.reasonFlagged,
+ followUpAt: decision.followUpAt,
+ },
+ })
+ .beforeClosed()
+ .subscribe(async ({ isEditing, reasonFlagged, followUpAt, isSaving }: FlagDialogIO) => {
+ if (isSaving) {
+ const updateDto: UpdateNoticeOfIntentDecisionDto = {
+ isDraft: decision.isDraft,
+ isFlagged: true,
+ reasonFlagged,
+ flagEditedByUuid: this.profile?.uuid,
+ flagEditedAt: moment().toDate().getTime(),
+ };
+
+ if (!isEditing) {
+ updateDto.flaggedByUuid = this.profile?.uuid;
+ }
+
+ if (followUpAt !== undefined) {
+ updateDto.followUpAt = followUpAt;
+ }
+
+ await this.decisionService.update(decision.uuid, updateDto);
+ await this.loadDecisions(this.fileNumber);
+ }
+ });
+ }
+
+ async unflag(decision: LoadingDecision, index: number) {
+ this.confirmationDialogService
+ .openDialog({
+ title: `Unflag Decision #${index}`,
+ body: `Warning: Only remove if flagged in error.
+
+
+ This action will also remove the follow-up date and explanatory text
+ associated with the flag and cannot be undone.
+
+
+ Are you sure you want to remove the flag?`,
+ })
+ .subscribe(async (confirmed) => {
+ if (confirmed) {
+ await this.decisionService.update(decision.uuid, {
+ isDraft: decision.isDraft,
+ isFlagged: false,
+ reasonFlagged: null,
+ followUpAt: null,
+ flaggedByUuid: null,
+ flagEditedByUuid: null,
+ flagEditedAt: null,
+ });
+ await this.loadDecisions(this.fileNumber);
+ }
+ });
+ }
+
+ formatDate(timestamp?: number | null, includeTime = false): string {
+ if (timestamp === undefined || timestamp === null) {
+ return '';
+ }
+
+ return moment(new Date(timestamp)).format(`YYYY-MMM-DD ${includeTime ? 'hh:mm:ss A' : ''}`);
+ }
}
diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts
index 38fc33418a..279d83e250 100644
--- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts
+++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts
@@ -1,3 +1,4 @@
+import { UserDto } from '../../../user/user.dto';
import { BaseCodeDto } from '../../../../shared/dto/base.dto';
import { CardDto } from '../../../card/card.dto';
import { ApplicationTypeDto } from '../../application-code.dto';
@@ -30,6 +31,12 @@ export interface UpdateApplicationDecisionDto {
conditions?: UpdateApplicationDecisionConditionDto[];
isDraft?: boolean;
ccEmails?: string[];
+ isFlagged?: boolean;
+ reasonFlagged?: string | null;
+ followUpAt?: number | null;
+ flaggedByUuid?: string | null;
+ flagEditedByUuid?: string | null;
+ flagEditedAt?: number | null;
}
export interface CreateApplicationDecisionDto extends UpdateApplicationDecisionDto {
@@ -76,6 +83,12 @@ export interface ApplicationDecisionDto {
conditions: ApplicationDecisionConditionDto[];
wasReleased: boolean;
conditionsCards: ApplicationDecisionConditionCardDto[];
+ isFlagged: boolean;
+ reasonFlagged: string | null;
+ followUpAt: number | null;
+ flaggedBy: UserDto | null;
+ flagEditedBy: UserDto | null;
+ flagEditedAt: number | null;
}
export interface LinkedResolutionDto {
@@ -328,6 +341,7 @@ export interface ApplicationDecisionConditionCardBoardDto {
card: CardDto;
decisionUuid: string;
decisionOrder: number;
+ decisionIsFlagged: boolean;
fileNumber: string;
applicant: string;
type?: ApplicationTypeDto;
diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision.dto.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision.dto.ts
index ac2468c1ac..dfe3fb922c 100644
--- a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision.dto.ts
+++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision.dto.ts
@@ -1,3 +1,4 @@
+import { UserDto } from '../../user/user.dto';
import { BaseCodeDto } from '../../../shared/dto/base.dto';
import { DateLabel, DateType } from '../../application/decision/application-decision-v2/application-decision-v2.dto';
@@ -18,6 +19,12 @@ export interface UpdateNoticeOfIntentDecisionDto {
decisionComponents?: NoticeOfIntentDecisionComponentDto[];
conditions?: UpdateNoticeOfIntentDecisionConditionDto[];
ccEmails?: string[];
+ isFlagged?: boolean;
+ reasonFlagged?: string | null;
+ followUpAt?: number | null;
+ flaggedByUuid?: string | null;
+ flagEditedByUuid?: string | null;
+ flagEditedAt?: number | null;
}
export interface CreateNoticeOfIntentDecisionDto extends UpdateNoticeOfIntentDecisionDto {
@@ -56,6 +63,12 @@ export interface NoticeOfIntentDecisionDto {
modifiedByResolutions?: string[];
components: NoticeOfIntentDecisionComponentDto[];
conditions: NoticeOfIntentDecisionConditionDto[];
+ isFlagged: boolean;
+ reasonFlagged: string | null;
+ followUpAt: number | null;
+ flaggedBy: UserDto | null;
+ flagEditedBy: UserDto | null;
+ flagEditedAt: number | null;
}
export interface NoticeOfIntentDecisionDocumentDto {
diff --git a/alcs-frontend/src/app/shared/card/card.component.html b/alcs-frontend/src/app/shared/card/card.component.html
index efd4cf35a5..8ec87a71bb 100644
--- a/alcs-frontend/src/app/shared/card/card.component.html
+++ b/alcs-frontend/src/app/shared/card/card.component.html
@@ -66,15 +66,25 @@
>
+
-
+
+
diff --git a/alcs-frontend/src/app/shared/card/card.component.scss b/alcs-frontend/src/app/shared/card/card.component.scss
index 0a55622d4a..f910aff950 100644
--- a/alcs-frontend/src/app/shared/card/card.component.scss
+++ b/alcs-frontend/src/app/shared/card/card.component.scss
@@ -141,3 +141,12 @@ mat-card {
.ellipsis {
@include text-ellipsis($lines: 2);
}
+
+.flag-avatar-container {
+ display: flex;
+ gap: 10px;
+}
+
+.flag-icon {
+ color: blue;
+}
diff --git a/alcs-frontend/src/app/shared/card/card.component.ts b/alcs-frontend/src/app/shared/card/card.component.ts
index 3d5e797330..cd5ecda1c7 100644
--- a/alcs-frontend/src/app/shared/card/card.component.ts
+++ b/alcs-frontend/src/app/shared/card/card.component.ts
@@ -30,6 +30,7 @@ export interface CardData {
maxActiveDays?: number;
legacyId?: string;
showDueDate?: boolean;
+ decisionIsFlagged?: boolean;
}
export interface ConditionCardData extends CardData {
@@ -62,8 +63,9 @@ const lineHeight = 24;
styleUrls: ['./card.component.scss'],
})
export class CardComponent implements OnInit {
- @Input() cardData!: CardData | ConditionCardData;
+ CardType = CardType;
+ @Input() cardData!: CardData | ConditionCardData;
@Output() cardSelected = new EventEmitter();
isConditionCard = false;
diff --git a/alcs-frontend/src/app/shared/flag-dialog/flag-dialog.component.html b/alcs-frontend/src/app/shared/flag-dialog/flag-dialog.component.html
new file mode 100644
index 0000000000..f87e000c61
--- /dev/null
+++ b/alcs-frontend/src/app/shared/flag-dialog/flag-dialog.component.html
@@ -0,0 +1,31 @@
+
+
{{ data.isEditing ? 'Edit' : '' }} Flag Decision #{{ data.decisionNumber }}
+
+
+
+
+ Flagged for condition follow-up because:
+
+
+
+
+ Follow-Up Date
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/alcs-frontend/src/app/shared/flag-dialog/flag-dialog.component.scss b/alcs-frontend/src/app/shared/flag-dialog/flag-dialog.component.scss
new file mode 100644
index 0000000000..ea27ef9f6a
--- /dev/null
+++ b/alcs-frontend/src/app/shared/flag-dialog/flag-dialog.component.scss
@@ -0,0 +1,24 @@
+@use '../../../styles/colors.scss';
+
+.content {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+
+ color: colors.$black;
+
+ height: 100%;
+ margin-top: 24px;
+}
+
+.reason-flagged {
+ margin-top: 5px;
+}
+
+.follow-up-at {
+ align-self: flex-start;
+}
+
+.button-container {
+ margin: 16px;
+}
diff --git a/alcs-frontend/src/app/shared/flag-dialog/flag-dialog.component.spec.ts b/alcs-frontend/src/app/shared/flag-dialog/flag-dialog.component.spec.ts
new file mode 100644
index 0000000000..3673e6f9d6
--- /dev/null
+++ b/alcs-frontend/src/app/shared/flag-dialog/flag-dialog.component.spec.ts
@@ -0,0 +1,56 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { createMock, DeepMocked } from '@golevelup/ts-jest';
+import { BehaviorSubject } from 'rxjs';
+import { ApplicationSubmissionStatusService } from '../../services/application/application-submission-status/application-submission-status.service';
+import {
+ ApplicationDecisionDto,
+ ApplicationDecisionWithLinkedResolutionDto,
+} from '../../services/application/decision/application-decision-v2/application-decision-v2.dto';
+import { ApplicationDecisionV2Service } from '../../services/application/decision/application-decision-v2/application-decision-v2.service';
+import { FlagDialogComponent } from './flag-dialog.component';
+
+describe('FlagDialogComponent', () => {
+ let component: FlagDialogComponent;
+ let fixture: ComponentFixture;
+ let mockApplicationDecisionV2Service: DeepMocked;
+ let mockSubmissionStatusService: DeepMocked;
+
+ beforeEach(async () => {
+ mockApplicationDecisionV2Service = createMock();
+ mockApplicationDecisionV2Service.$decision = new BehaviorSubject(undefined);
+ mockApplicationDecisionV2Service.$decisions = new BehaviorSubject([]);
+ mockSubmissionStatusService = createMock();
+
+ await TestBed.configureTestingModule({
+ declarations: [FlagDialogComponent],
+ providers: [
+ {
+ provide: ApplicationDecisionV2Service,
+ useValue: mockApplicationDecisionV2Service,
+ },
+ {
+ provide: ApplicationSubmissionStatusService,
+ useValue: mockSubmissionStatusService,
+ },
+ { provide: MatDialogRef, useValue: {} },
+ {
+ provide: MAT_DIALOG_DATA,
+ useValue: {
+ fileNumber: '12313',
+ },
+ },
+ ],
+ schemas: [NO_ERRORS_SCHEMA],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(FlagDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/alcs-frontend/src/app/shared/flag-dialog/flag-dialog.component.ts b/alcs-frontend/src/app/shared/flag-dialog/flag-dialog.component.ts
new file mode 100644
index 0000000000..d9c4a4d0b8
--- /dev/null
+++ b/alcs-frontend/src/app/shared/flag-dialog/flag-dialog.component.ts
@@ -0,0 +1,54 @@
+import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { Subject } from 'rxjs';
+import moment, { Moment } from 'moment';
+
+export interface FlagDialogIO {
+ isEditing?: boolean;
+ decisionNumber?: number;
+ reasonFlagged?: string;
+ followUpAt?: number | null;
+ isSaving?: boolean;
+}
+
+@Component({
+ selector: 'app-flag-dialog',
+ templateUrl: './flag-dialog.component.html',
+ styleUrls: ['./flag-dialog.component.scss'],
+})
+export class FlagDialogComponent implements OnInit, OnDestroy {
+ $destroy = new Subject();
+
+ followUpAt?: Moment | null;
+
+ constructor(
+ public matDialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA)
+ protected data: FlagDialogIO,
+ ) {}
+
+ ngOnInit(): void {
+ if (this.data.followUpAt) {
+ this.followUpAt = moment(this.data.followUpAt);
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.$destroy.next();
+ this.$destroy.complete();
+ }
+
+ save() {
+ const output: FlagDialogIO = {
+ isEditing: this.data.isEditing,
+ reasonFlagged: this.data.reasonFlagged,
+ isSaving: true,
+ };
+
+ if (this.followUpAt !== undefined) {
+ output.followUpAt = this.followUpAt ? this.followUpAt.toDate().getTime() : null;
+ }
+
+ this.matDialogRef.close(output);
+ }
+}
diff --git a/alcs-frontend/src/app/shared/shared.module.ts b/alcs-frontend/src/app/shared/shared.module.ts
index 701c6dabb6..7f1b310639 100644
--- a/alcs-frontend/src/app/shared/shared.module.ts
+++ b/alcs-frontend/src/app/shared/shared.module.ts
@@ -80,6 +80,7 @@ import { TagChipComponent } from './tags/tag-chip/tag-chip.component';
import { DomSanitizer } from '@angular/platform-browser';
import { CommissionerTagsHeaderComponent } from './tags/commissioner-tags-header/commissioner-tags-header.component';
import { DocumentUploadDialogComponent } from './document-upload-dialog/document-upload-dialog.component';
+import { FlagDialogComponent } from './flag-dialog/flag-dialog.component';
@NgModule({
declarations: [
@@ -125,6 +126,7 @@ import { DocumentUploadDialogComponent } from './document-upload-dialog/document
TagChipComponent,
CommissionerTagsHeaderComponent,
DocumentUploadDialogComponent,
+ FlagDialogComponent,
],
imports: [
CommonModule,
@@ -230,6 +232,7 @@ import { DocumentUploadDialogComponent } from './document-upload-dialog/document
TagsHeaderComponent,
TagChipComponent,
DocumentUploadDialogComponent,
+ FlagDialogComponent,
],
})
export class SharedModule {
@@ -238,6 +241,10 @@ export class SharedModule {
'cancel_filled',
domSanitizer.bypassSecurityTrustResourceUrl('/assets/icons/cancel_filled.svg'),
);
+ matIconRegistry.addSvgIcon(
+ 'personal_places',
+ domSanitizer.bypassSecurityTrustResourceUrl('/assets/icons/personal_places.svg'),
+ );
}
static forRoot(): ModuleWithProviders {
return {
diff --git a/alcs-frontend/src/assets/icons/personal_places.svg b/alcs-frontend/src/assets/icons/personal_places.svg
new file mode 100644
index 0000000000..ffdb3a6601
--- /dev/null
+++ b/alcs-frontend/src/assets/icons/personal_places.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.dto.ts
index ccea5e9e16..d0387a61a1 100644
--- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.dto.ts
+++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.dto.ts
@@ -76,6 +76,9 @@ export class ApplicationDecisionConditionCardBoardDto {
@IsNumber()
decisionOrder: number;
+ @IsBoolean()
+ decisionIsFlagged: boolean;
+
@IsString()
fileNumber: string;
diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts
index 6fbc12ddcf..e06f83986f 100644
--- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts
+++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts
@@ -53,6 +53,7 @@ import { ApplicationDecisionConditionDateController } from '../application-decis
import { ApplicationDecisionConditionCardController } from '../application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller';
import { ApplicationDecisionConditionCard } from '../application-decision-condition/application-decision-condition-card/application-decision-condition-card.entity';
import { ApplicationDecisionConditionCardService } from '../application-decision-condition/application-decision-condition-card/application-decision-condition-card.service';
+import { User } from 'apps/alcs/src/user/user.entity';
@Module({
imports: [
@@ -82,6 +83,7 @@ import { ApplicationDecisionConditionCardService } from '../application-decision
ApplicationDecisionConditionComponentPlanNumber,
ApplicationBoundaryAmendment,
ApplicationDecisionConditionCard,
+ User,
]),
forwardRef(() => BoardModule),
forwardRef(() => ApplicationModule),
diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.spec.ts
index cc7217fbd4..b47a97faf5 100644
--- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.spec.ts
+++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.spec.ts
@@ -26,6 +26,7 @@ import { ApplicationDecisionComponentDto } from './component/application-decisio
import { ApplicationDecisionComponent } from './component/application-decision-component.entity';
import { ApplicationDecisionComponentService } from './component/application-decision-component.service';
import { ApplicationDecisionConditionCardService } from '../../application-decision-condition/application-decision-condition-card/application-decision-condition-card.service';
+import { User } from '../../../../user/user.entity';
describe('ApplicationDecisionV2Service', () => {
let service: ApplicationDecisionV2Service;
@@ -40,6 +41,7 @@ describe('ApplicationDecisionV2Service', () => {
let mockDecisionComponentService: DeepMocked;
let mockDecisionConditionService: DeepMocked;
let mockNaruSubtypeRepository: DeepMocked>;
+ let mockUserRepository: DeepMocked>;
let mockApplicationSubmissionStatusService: DeepMocked;
let mockApplicationDecisionConditionCardService: DeepMocked;
let mockdataSource: DeepMocked;
@@ -55,6 +57,7 @@ describe('ApplicationDecisionV2Service', () => {
mockDecisionOutcomeRepository = createMock>();
mockDecisionMakerCodeRepository = createMock>();
mockCeoCriterionCodeRepository = createMock>();
+ mockUserRepository = createMock>();
mockApplicationDecisionComponentTypeRepository = createMock();
mockDecisionComponentService = createMock();
mockDecisionConditionService = createMock();
@@ -95,6 +98,10 @@ describe('ApplicationDecisionV2Service', () => {
provide: getRepositoryToken(NaruSubtype),
useValue: mockNaruSubtypeRepository,
},
+ {
+ provide: getRepositoryToken(User),
+ useValue: mockUserRepository,
+ },
{
provide: ApplicationService,
useValue: mockApplicationService,
diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts
index 74adee5092..19fa6cb646 100644
--- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts
+++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts
@@ -52,6 +52,8 @@ export class ApplicationDecisionV2Service {
private decisionConditionTypeRepository: Repository,
@InjectRepository(NaruSubtype)
private naruNaruSubtypeRepository: Repository,
+ @InjectRepository(User)
+ private userRepository: Repository,
private applicationService: ApplicationService,
private documentService: DocumentService,
private decisionComponentService: ApplicationDecisionComponentService,
@@ -280,6 +282,32 @@ export class ApplicationDecisionV2Service {
existingDecision.linkedResolutionOutcomeCode = updateDto.linkedResolutionOutcomeCode;
existingDecision.emailSent = updateDto.emailSent;
existingDecision.ccEmails = filterUndefined(updateDto.ccEmails, existingDecision.ccEmails);
+ existingDecision.isFlagged = updateDto.isFlagged;
+ existingDecision.reasonFlagged = updateDto.reasonFlagged;
+ existingDecision.followUpAt = formatIncomingDate(updateDto.followUpAt);
+ existingDecision.flagEditedAt = formatIncomingDate(updateDto.flagEditedAt);
+
+ if (updateDto.flaggedByUuid !== undefined) {
+ existingDecision.flaggedBy =
+ updateDto.flaggedByUuid === null
+ ? null
+ : await this.userRepository.findOne({
+ where: {
+ uuid: updateDto.flaggedByUuid,
+ },
+ });
+ }
+
+ if (updateDto.flagEditedByUuid !== undefined) {
+ existingDecision.flagEditedBy =
+ updateDto.flagEditedByUuid === null
+ ? null
+ : await this.userRepository.findOne({
+ where: {
+ uuid: updateDto.flagEditedByUuid,
+ },
+ });
+ }
if (updateDto.outcomeCode) {
existingDecision.outcome = await this.getOutcomeByCode(updateDto.outcomeCode);
diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision.dto.ts
index 5b94169d02..76855ba9dc 100644
--- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision.dto.ts
+++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision.dto.ts
@@ -15,6 +15,8 @@ import {
ApplicationDecisionConditionCardDto,
ApplicationDecisionConditionCardUuidDto,
} from '../../application-decision-condition/application-decision-condition-card/application-decision-condition-card.dto';
+import { Type } from 'class-transformer';
+import { UserDto } from '../../../../user/user.dto';
export class UpdateApplicationDecisionDto {
@IsNumber()
@@ -110,6 +112,30 @@ export class UpdateApplicationDecisionDto {
@IsOptional()
@IsArray()
ccEmails?: string[];
+
+ @IsOptional()
+ @IsBoolean()
+ isFlagged?: boolean;
+
+ @IsOptional()
+ @IsString()
+ reasonFlagged?: string | null;
+
+ @IsOptional()
+ @IsNumber()
+ followUpAt?: number | null;
+
+ @IsOptional()
+ @IsString()
+ flaggedByUuid?: string | null;
+
+ @IsOptional()
+ @IsString()
+ flagEditedByUuid?: string | null;
+
+ @IsOptional()
+ @IsNumber()
+ flagEditedAt?: number | null;
}
export class CreateApplicationDecisionDto extends UpdateApplicationDecisionDto {
@@ -228,6 +254,24 @@ export class ApplicationDecisionDto {
@AutoMap(() => [ApplicationDecisionConditionCardUuidDto])
conditionCards?: ApplicationDecisionConditionCardUuidDto[];
+
+ @AutoMap(() => Boolean)
+ isFlagged: boolean;
+
+ @AutoMap(() => String)
+ reasonFlagged: string | null;
+
+ @AutoMap(() => Number)
+ followUpAt: number | null;
+
+ @AutoMap(() => UserDto)
+ flaggedBy: UserDto | null;
+
+ @AutoMap(() => UserDto)
+ flagEditedBy: UserDto | null;
+
+ @AutoMap(() => Number)
+ flagEditedAt: number | null;
}
export class LinkedResolutionDto {
diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-decision.entity.ts
index 5b0591585c..f657dfe4f7 100644
--- a/services/apps/alcs/src/alcs/application-decision/application-decision.entity.ts
+++ b/services/apps/alcs/src/alcs/application-decision/application-decision.entity.ts
@@ -22,6 +22,7 @@ import { ApplicationDecisionComponent } from './application-decision-v2/applicat
import { ApplicationModification } from './application-modification/application-modification.entity';
import { ApplicationReconsideration } from './application-reconsideration/application-reconsideration.entity';
import { ApplicationDecisionConditionCard } from './application-decision-condition/application-decision-condition-card/application-decision-condition-card.entity';
+import { User } from '../../user/user.entity';
@Entity({
comment: 'Decisions saved to applications, incl. those linked to the recon/modification request',
@@ -233,4 +234,28 @@ export class ApplicationDecision extends Base {
cascade: ['insert', 'update'],
})
conditionCards: ApplicationDecisionConditionCard[];
+
+ @AutoMap()
+ @Column({ default: false })
+ isFlagged: boolean;
+
+ @AutoMap(() => String)
+ @Column({ type: 'text', nullable: true })
+ reasonFlagged: string | null;
+
+ @AutoMap(() => Date)
+ @Column({ type: 'timestamptz', nullable: true })
+ followUpAt: Date | null;
+
+ @AutoMap(() => User)
+ @ManyToOne(() => User, { nullable: true, eager: true })
+ flaggedBy: User | null;
+
+ @AutoMap(() => User)
+ @ManyToOne(() => User, { nullable: true, eager: true })
+ flagEditedBy: User | null;
+
+ @AutoMap(() => Date)
+ @Column({ type: 'timestamptz', nullable: true })
+ flagEditedAt: Date | null;
}
diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts
index 6dc21dc9ce..3532da4bf4 100644
--- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts
+++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts
@@ -1,7 +1,4 @@
-import {
- ServiceNotFoundException,
- ServiceValidationException,
-} from '@app/common/exceptions/base.exception';
+import { ServiceNotFoundException, ServiceValidationException } from '@app/common/exceptions/base.exception';
import { classes } from 'automapper-classes';
import { AutomapperModule } from 'automapper-nestjs';
import { createMock, DeepMocked } from '@golevelup/nestjs-testing';
@@ -22,27 +19,20 @@ import { NoticeOfIntentDecisionCondition } from '../notice-of-intent-decision-co
import { NoticeOfIntentDecisionConditionService } from '../notice-of-intent-decision-condition/notice-of-intent-decision-condition.service';
import { NoticeOfIntentDecisionDocument } from '../notice-of-intent-decision-document/notice-of-intent-decision-document.entity';
import { NoticeOfIntentDecisionOutcome } from '../notice-of-intent-decision-outcome.entity';
-import {
- CreateNoticeOfIntentDecisionDto,
- UpdateNoticeOfIntentDecisionDto,
-} from '../notice-of-intent-decision.dto';
+import { CreateNoticeOfIntentDecisionDto, UpdateNoticeOfIntentDecisionDto } from '../notice-of-intent-decision.dto';
import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity';
import { NoticeOfIntentDecisionV2Service } from './notice-of-intent-decision-v2.service';
+import { User } from '../../../user/user.entity';
describe('NoticeOfIntentDecisionV2Service', () => {
let service: NoticeOfIntentDecisionV2Service;
let mockDecisionRepository: DeepMocked>;
- let mockDecisionDocumentRepository: DeepMocked<
- Repository
- >;
- let mockDecisionOutcomeRepository: DeepMocked<
- Repository
- >;
+ let mockDecisionDocumentRepository: DeepMocked>;
+ let mockDecisionOutcomeRepository: DeepMocked>;
let mockNoticeOfIntentService: DeepMocked;
let mockDocumentService: DeepMocked;
- let mockNoticeOfIntentDecisionComponentTypeRepository: DeepMocked<
- Repository
- >;
+ let mockNoticeOfIntentDecisionComponentTypeRepository: DeepMocked>;
+ let mockUserRepository: DeepMocked>;
let mockDecisionComponentService: DeepMocked;
let mockDecisionConditionService: DeepMocked;
let mockNoticeOfIntentSubmissionStatusService: DeepMocked;
@@ -55,10 +45,9 @@ describe('NoticeOfIntentDecisionV2Service', () => {
mockNoticeOfIntentService = createMock();
mockDocumentService = createMock();
mockDecisionRepository = createMock>();
- mockDecisionDocumentRepository =
- createMock>();
- mockDecisionOutcomeRepository =
- createMock>();
+ mockDecisionDocumentRepository = createMock>();
+ mockDecisionOutcomeRepository = createMock>();
+ mockUserRepository = createMock>();
mockNoticeOfIntentDecisionComponentTypeRepository = createMock();
mockDecisionComponentService = createMock();
mockDecisionConditionService = createMock();
@@ -97,6 +86,10 @@ describe('NoticeOfIntentDecisionV2Service', () => {
provide: getRepositoryToken(NoticeOfIntentDecisionComponentType),
useValue: mockNoticeOfIntentDecisionComponentTypeRepository,
},
+ {
+ provide: getRepositoryToken(User),
+ useValue: mockUserRepository,
+ },
{
provide: NoticeOfIntentDecisionComponentService,
useValue: mockDecisionComponentService,
@@ -120,9 +113,7 @@ describe('NoticeOfIntentDecisionV2Service', () => {
],
}).compile();
- service = module.get(
- NoticeOfIntentDecisionV2Service,
- );
+ service = module.get(NoticeOfIntentDecisionV2Service);
mockNoticeOfIntent = new NoticeOfIntent({
uuid: '1111-1111-1111-1111',
@@ -138,9 +129,7 @@ describe('NoticeOfIntentDecisionV2Service', () => {
mockDecisionDocumentRepository.find.mockResolvedValue([]);
- mockNoticeOfIntentService.getByFileNumber.mockResolvedValue(
- mockNoticeOfIntent,
- );
+ mockNoticeOfIntentService.getByFileNumber.mockResolvedValue(mockNoticeOfIntent);
mockNoticeOfIntentService.update.mockResolvedValue({} as any);
mockNoticeOfIntentService.updateByUuid.mockResolvedValue({} as any);
mockNoticeOfIntentService.getUuid.mockResolvedValue('uuid');
@@ -148,14 +137,10 @@ describe('NoticeOfIntentDecisionV2Service', () => {
mockDecisionOutcomeRepository.find.mockResolvedValue([]);
mockDecisionOutcomeRepository.findOneOrFail.mockResolvedValue({} as any);
- mockNoticeOfIntentDecisionComponentTypeRepository.find.mockResolvedValue(
- [],
- );
+ mockNoticeOfIntentDecisionComponentTypeRepository.find.mockResolvedValue([]);
mockDecisionComponentService.createOrUpdate.mockResolvedValue([]);
mockDecisionConditionService.remove.mockResolvedValue({} as any);
- mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber.mockResolvedValue(
- {} as any,
- );
+ mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber.mockResolvedValue({} as any);
});
describe('NoticeOfIntentDecisionService Core Tests', () => {
@@ -164,9 +149,7 @@ describe('NoticeOfIntentDecisionV2Service', () => {
});
it('should get decisions by notice of intent', async () => {
- const result = await service.getByFileNumber(
- mockNoticeOfIntent.fileNumber,
- );
+ const result = await service.getByFileNumber(mockNoticeOfIntent.fileNumber);
expect(result).toStrictEqual([mockDecision]);
});
@@ -192,18 +175,11 @@ describe('NoticeOfIntentDecisionV2Service', () => {
expect(mockDecisionRepository.save.mock.calls[0][0].modifies).toBeNull();
expect(mockDecisionRepository.softRemove).toBeCalledTimes(1);
expect(mockNoticeOfIntentService.updateByUuid).toHaveBeenCalledTimes(1);
- expect(mockNoticeOfIntentService.updateByUuid).toHaveBeenCalledWith(
- mockNoticeOfIntent.uuid,
- {
- decisionDate: null,
- },
- );
- expect(
- mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber,
- ).toBeCalledTimes(1);
- expect(
- mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber,
- ).toBeCalledWith(
+ expect(mockNoticeOfIntentService.updateByUuid).toHaveBeenCalledWith(mockNoticeOfIntent.uuid, {
+ decisionDate: null,
+ });
+ expect(mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber).toBeCalledTimes(1);
+ expect(mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber).toBeCalledWith(
mockNoticeOfIntent.fileNumber,
NOI_SUBMISSION_STATUS.ALC_DECISION,
null,
@@ -270,20 +246,14 @@ describe('NoticeOfIntentDecisionV2Service', () => {
expect(finalDecision.decisionMaker).toEqual(decision.decisionMaker);
expect(finalDecision.outcomeCode).toEqual(decision.outcomeCode);
- expect(finalDecision.decisionMakerName).toEqual(
- decision.decisionMakerName,
- );
- expect(finalDecision.isSubjectToConditions).toEqual(
- decision.isSubjectToConditions,
- );
+ expect(finalDecision.decisionMakerName).toEqual(decision.decisionMakerName);
+ expect(finalDecision.isSubjectToConditions).toEqual(decision.isSubjectToConditions);
expect(finalDecision.components?.length).toEqual(1);
expect(finalDecision.conditions?.length).toEqual(1);
});
it('should fail create a decision if the resolution number is already in use', async () => {
- mockDecisionRepository.findOne.mockResolvedValue(
- {} as NoticeOfIntentDecision,
- );
+ mockDecisionRepository.findOne.mockResolvedValue({} as NoticeOfIntentDecision);
mockDecisionRepository.exist.mockResolvedValueOnce(false);
const decisionDate = new Date(2022, 2, 2, 2, 2, 2, 2);
@@ -296,9 +266,7 @@ describe('NoticeOfIntentDecisionV2Service', () => {
isDraft: true,
} as CreateNoticeOfIntentDecisionDto;
- await expect(
- service.create(decisionToCreate, mockNoticeOfIntent, undefined),
- ).rejects.toMatchObject(
+ await expect(service.create(decisionToCreate, mockNoticeOfIntent, undefined)).rejects.toMatchObject(
new ServiceValidationException(
`Resolution number #${decisionToCreate.resolutionNumber}/${decisionToCreate.resolutionYear} is already in use`,
),
@@ -324,12 +292,8 @@ describe('NoticeOfIntentDecisionV2Service', () => {
isDraft: true,
} as CreateNoticeOfIntentDecisionDto;
- await expect(
- service.create(decisionToCreate, mockNoticeOfIntent, undefined),
- ).rejects.toMatchObject(
- new ServiceValidationException(
- 'Draft decision already exists for this notice of intent.',
- ),
+ await expect(service.create(decisionToCreate, mockNoticeOfIntent, undefined)).rejects.toMatchObject(
+ new ServiceValidationException('Draft decision already exists for this notice of intent.'),
);
expect(mockDecisionRepository.save).toBeCalledTimes(0);
@@ -353,9 +317,7 @@ describe('NoticeOfIntentDecisionV2Service', () => {
expect(mockDecisionRepository.save).toBeCalledTimes(1);
expect(mockNoticeOfIntentService.update).not.toHaveBeenCalled();
- expect(
- mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber,
- ).not.toHaveBeenCalled();
+ expect(mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber).not.toHaveBeenCalled();
});
it('should update the decision and update the notice of intent and submission status if it was the only decision', async () => {
@@ -387,18 +349,11 @@ describe('NoticeOfIntentDecisionV2Service', () => {
expect(mockDecisionRepository.findOne).toBeCalledTimes(2);
expect(mockDecisionRepository.save).toHaveBeenCalledTimes(1);
expect(mockNoticeOfIntentService.updateByUuid).toHaveBeenCalledTimes(1);
- expect(mockNoticeOfIntentService.updateByUuid).toHaveBeenCalledWith(
- mockNoticeOfIntent.uuid,
- {
- decisionDate,
- },
- );
- expect(
- mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber,
- ).toBeCalledTimes(1);
- expect(
- mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber,
- ).toBeCalledWith(
+ expect(mockNoticeOfIntentService.updateByUuid).toHaveBeenCalledWith(mockNoticeOfIntent.uuid, {
+ decisionDate,
+ });
+ expect(mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber).toBeCalledTimes(1);
+ expect(mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber).toBeCalledWith(
mockNoticeOfIntent.fileNumber,
NOI_SUBMISSION_STATUS.ALC_DECISION,
decisionDate,
@@ -418,18 +373,11 @@ describe('NoticeOfIntentDecisionV2Service', () => {
expect(mockDecisionRepository.findOne).toBeCalledTimes(2);
expect(mockDecisionRepository.save).toBeCalledTimes(1);
expect(mockNoticeOfIntentService.updateByUuid).toHaveBeenCalledTimes(1);
- expect(mockNoticeOfIntentService.updateByUuid).toBeCalledWith(
- '1111-1111-1111-1111',
- {
- decisionDate: null,
- },
- );
- expect(
- mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber,
- ).toBeCalledTimes(1);
- expect(
- mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber,
- ).toBeCalledWith(
+ expect(mockNoticeOfIntentService.updateByUuid).toBeCalledWith('1111-1111-1111-1111', {
+ decisionDate: null,
+ });
+ expect(mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber).toBeCalledTimes(1);
+ expect(mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber).toBeCalledWith(
mockNoticeOfIntent.fileNumber,
NOI_SUBMISSION_STATUS.ALC_DECISION,
null,
@@ -443,10 +391,7 @@ describe('NoticeOfIntentDecisionV2Service', () => {
});
secondDecision.isDraft = true;
secondDecision.uuid = 'second-uuid';
- mockDecisionRepository.find.mockResolvedValue([
- secondDecision,
- mockDecision,
- ]);
+ mockDecisionRepository.find.mockResolvedValue([secondDecision, mockDecision]);
mockDecisionRepository.findOne.mockResolvedValue(secondDecision);
const decisionUpdate: UpdateNoticeOfIntentDecisionDto = {
@@ -459,9 +404,7 @@ describe('NoticeOfIntentDecisionV2Service', () => {
expect(mockDecisionRepository.findOne).toBeCalledTimes(2);
expect(mockDecisionRepository.save).toBeCalledTimes(1);
expect(mockNoticeOfIntentService.update).not.toHaveBeenCalled();
- expect(
- mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber,
- ).not.toHaveBeenCalled();
+ expect(mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber).not.toHaveBeenCalled();
});
it('should fail on update if the decision is not found', async () => {
@@ -472,16 +415,10 @@ describe('NoticeOfIntentDecisionV2Service', () => {
outcomeCode: 'New Outcome',
isDraft: true,
};
- const promise = service.update(
- nonExistantUuid,
- decisionUpdate,
- undefined,
- );
+ const promise = service.update(nonExistantUuid, decisionUpdate, undefined);
await expect(promise).rejects.toMatchObject(
- new ServiceNotFoundException(
- `Decision with UUID ${nonExistantUuid} not found`,
- ),
+ new ServiceNotFoundException(`Decision with UUID ${nonExistantUuid} not found`),
);
expect(mockDecisionRepository.save).toBeCalledTimes(0);
});
@@ -519,9 +456,7 @@ describe('NoticeOfIntentDecisionV2Service', () => {
});
it('should call the repository to check if portal user can download document', async () => {
- mockDecisionDocumentRepository.findOne.mockResolvedValue(
- new NoticeOfIntentDecisionDocument(),
- );
+ mockDecisionDocumentRepository.findOne.mockResolvedValue(new NoticeOfIntentDecisionDocument());
mockDocumentService.getDownloadUrl.mockResolvedValue('');
await service.getDownloadForPortal('fake-uuid');
@@ -531,9 +466,7 @@ describe('NoticeOfIntentDecisionV2Service', () => {
it('should throw an exception when attaching a document to a non-existent decision', async () => {
mockDecisionRepository.findOne.mockResolvedValue(null);
- await expect(
- service.attachDocument('uuid', {} as any, {} as any),
- ).rejects.toMatchObject(
+ await expect(service.attachDocument('uuid', {} as any, {} as any)).rejects.toMatchObject(
new ServiceNotFoundException(`Decision with UUID uuid not found`),
);
expect(mockDocumentService.create).not.toHaveBeenCalled();
@@ -543,17 +476,13 @@ describe('NoticeOfIntentDecisionV2Service', () => {
mockDecisionDocumentRepository.softRemove.mockResolvedValue({} as any);
await service.deleteDocument('fake-uuid');
- expect(mockDecisionDocumentRepository.softRemove).toHaveBeenCalledTimes(
- 1,
- );
+ expect(mockDecisionDocumentRepository.softRemove).toHaveBeenCalledTimes(1);
});
it('should throw an exception when document not found for deletion', async () => {
mockDecisionDocumentRepository.findOne.mockResolvedValue(null);
await expect(service.deleteDocument('fake-uuid')).rejects.toMatchObject(
- new ServiceNotFoundException(
- `Failed to find document with uuid fake-uuid`,
- ),
+ new ServiceNotFoundException(`Failed to find document with uuid fake-uuid`),
);
expect(mockDocumentService.softRemove).not.toHaveBeenCalled();
});
@@ -578,9 +507,7 @@ describe('NoticeOfIntentDecisionV2Service', () => {
it('should throw an exception when document not found for download', async () => {
mockDecisionDocumentRepository.findOne.mockResolvedValue(null);
await expect(service.getDownloadUrl('fake-uuid')).rejects.toMatchObject(
- new ServiceNotFoundException(
- `Failed to find document with uuid fake-uuid`,
- ),
+ new ServiceNotFoundException(`Failed to find document with uuid fake-uuid`),
);
});
});
diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts
index 9de0ae2086..266938558d 100644
--- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts
+++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts
@@ -46,6 +46,8 @@ export class NoticeOfIntentDecisionV2Service {
private decisionComponentTypeRepository: Repository,
@InjectRepository(NoticeOfIntentDecisionConditionType)
private decisionConditionTypeRepository: Repository,
+ @InjectRepository(User)
+ private userRepository: Repository,
private noticeOfIntentService: NoticeOfIntentService,
private documentService: DocumentService,
private decisionComponentService: NoticeOfIntentDecisionComponentService,
@@ -220,6 +222,32 @@ export class NoticeOfIntentDecisionV2Service {
existingDecision.wasReleased || !updateDto.isDraft;
existingDecision.emailSent = updateDto.emailSent;
existingDecision.ccEmails = updateDto.ccEmails;
+ existingDecision.isFlagged = updateDto.isFlagged;
+ existingDecision.reasonFlagged = updateDto.reasonFlagged;
+ existingDecision.followUpAt = formatIncomingDate(updateDto.followUpAt);
+ existingDecision.flagEditedAt = formatIncomingDate(updateDto.flagEditedAt);
+
+ if (updateDto.flaggedByUuid !== undefined) {
+ existingDecision.flaggedBy =
+ updateDto.flaggedByUuid === null
+ ? null
+ : await this.userRepository.findOne({
+ where: {
+ uuid: updateDto.flaggedByUuid,
+ },
+ });
+ }
+
+ if (updateDto.flagEditedByUuid !== undefined) {
+ existingDecision.flagEditedBy =
+ updateDto.flagEditedByUuid === null
+ ? null
+ : await this.userRepository.findOne({
+ where: {
+ uuid: updateDto.flagEditedByUuid,
+ },
+ });
+ }
if (updateDto.outcomeCode) {
existingDecision.outcome = await this.getOutcomeByCode(
diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.dto.ts
index dffb2fa524..5697240abc 100644
--- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.dto.ts
+++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.dto.ts
@@ -17,6 +17,8 @@ import {
NoticeOfIntentDecisionConditionDto,
UpdateNoticeOfIntentDecisionConditionDto,
} from './notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto';
+import { UserDto } from '../../user/user.dto';
+import { Type } from 'class-transformer';
export class NoticeOfIntentDecisionOutcomeCodeDto extends BaseCodeDto {}
@@ -87,6 +89,30 @@ export class UpdateNoticeOfIntentDecisionDto {
@IsOptional()
@IsArray()
ccEmails?: string[];
+
+ @IsOptional()
+ @IsBoolean()
+ isFlagged?: boolean;
+
+ @IsOptional()
+ @IsString()
+ reasonFlagged?: string | null;
+
+ @IsOptional()
+ @IsNumber()
+ followUpAt?: number | null;
+
+ @IsOptional()
+ @IsString()
+ flaggedByUuid?: string | null;
+
+ @IsOptional()
+ @IsString()
+ flagEditedByUuid?: string | null;
+
+ @IsOptional()
+ @IsNumber()
+ flagEditedAt?: number | null;
}
export class CreateNoticeOfIntentDecisionDto extends UpdateNoticeOfIntentDecisionDto {
@@ -160,6 +186,24 @@ export class NoticeOfIntentDecisionDto {
@AutoMap(() => [NoticeOfIntentDecisionConditionDto])
conditions?: NoticeOfIntentDecisionConditionDto[];
+
+ @AutoMap(() => Boolean)
+ isFlagged: boolean;
+
+ @AutoMap(() => String)
+ reasonFlagged: string | null;
+
+ @AutoMap(() => Number)
+ followUpAt: number | null;
+
+ @AutoMap(() => UserDto)
+ flaggedBy: UserDto | null;
+
+ @AutoMap(() => UserDto)
+ flagEditedBy: UserDto | null;
+
+ @AutoMap(() => Number)
+ flagEditedAt: number | null;
}
export class NoticeOfIntentDecisionDocumentDto {
diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.entity.ts
index cae8d3168e..b0263bed7b 100644
--- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.entity.ts
+++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.entity.ts
@@ -17,6 +17,7 @@ import { NoticeOfIntentDecisionCondition } from './notice-of-intent-decision-con
import { NoticeOfIntentDecisionDocument } from './notice-of-intent-decision-document/notice-of-intent-decision-document.entity';
import { NoticeOfIntentDecisionOutcome } from './notice-of-intent-decision-outcome.entity';
import { NoticeOfIntentModification } from './notice-of-intent-modification/notice-of-intent-modification.entity';
+import { User } from '../../user/user.entity';
@Entity({
comment: 'Decisions saved to NOIs, linked to the modification request',
@@ -189,4 +190,28 @@ export class NoticeOfIntentDecision extends Base {
'This column is NOT related to any functionality in ALCS. It is only used for ETL and backtracking of imported data from OATS. It links oats.oats_alr_appl_decisions to alcs.notice_of_intent_decisions.',
})
oatsAlrApplDecisionId: number;
+
+ @AutoMap()
+ @Column({ default: false })
+ isFlagged: boolean;
+
+ @AutoMap(() => String)
+ @Column({ type: 'text', nullable: true })
+ reasonFlagged: string | null;
+
+ @AutoMap(() => Date)
+ @Column({ type: 'timestamptz', nullable: true })
+ followUpAt: Date | null;
+
+ @AutoMap(() => User)
+ @ManyToOne(() => User, { nullable: true, eager: true })
+ flaggedBy: User | null;
+
+ @AutoMap(() => User)
+ @ManyToOne(() => User, { nullable: true, eager: true })
+ flagEditedBy: User | null;
+
+ @AutoMap(() => Date)
+ @Column({ type: 'timestamptz', nullable: true })
+ flagEditedAt: Date | null;
}
diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts
index 1361b06718..c8afc76bad 100644
--- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts
+++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts
@@ -27,6 +27,7 @@ import { NoticeOfIntentModificationService } from './notice-of-intent-modificati
import { NoticeOfIntentDecisionConditionDate } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition-date/notice-of-intent-decision-condition-date.entity';
import { NoticeOfIntentDecisionConditionDateService } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition-date/notice-of-intent-decision-condition-date.service';
import { NoticeOfIntentDecisionConditionDateController } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition-date/notice-of-intent-decision-condition-date.controller';
+import { User } from '../../user/user.entity';
@Module({
imports: [
@@ -41,6 +42,7 @@ import { NoticeOfIntentDecisionConditionDateController } from './notice-of-inten
NoticeOfIntentDecisionCondition,
NoticeOfIntentDecisionConditionType,
NoticeOfIntentDecisionConditionDate,
+ User,
]),
forwardRef(() => BoardModule),
CardModule,
diff --git a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts
index 0bdf2ea357..5941057791 100644
--- a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts
+++ b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts
@@ -48,6 +48,8 @@ import {
ApplicationDecisionConditionCardDto,
ApplicationDecisionConditionCardUuidDto,
} from '../../alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.dto';
+import { UserDto } from '../../user/user.dto';
+import { User } from '../../user/user.entity';
@Injectable()
export class ApplicationDecisionProfile extends AutomapperProfile {
@@ -159,6 +161,22 @@ export class ApplicationDecisionProfile extends AutomapperProfile {
: [],
),
),
+ forMember(
+ (ad) => ad.followUpAt,
+ mapFrom((a) => a.followUpAt?.getTime()),
+ ),
+ forMember(
+ (ad) => ad.flaggedBy,
+ mapFrom((a) => (a.flaggedBy ? this.mapper.map(a.flaggedBy, User, UserDto) : a.flaggedBy)),
+ ),
+ forMember(
+ (ad) => ad.flagEditedBy,
+ mapFrom((a) => (a.flagEditedBy ? this.mapper.map(a.flagEditedBy, User, UserDto) : a.flagEditedBy)),
+ ),
+ forMember(
+ (ad) => ad.flagEditedAt,
+ mapFrom((a) => a.flagEditedAt?.getTime()),
+ ),
);
createMap(mapper, ApplicationDecisionOutcomeCode, ApplicationDecisionOutcomeCodeDto);
@@ -425,6 +443,10 @@ export class ApplicationDecisionProfile extends AutomapperProfile {
(dto) => dto.decisionUuid,
mapFrom((entity) => entity.decision.uuid),
),
+ forMember(
+ (dto) => dto.decisionIsFlagged,
+ mapFrom((entity) => entity.decision.isFlagged),
+ ),
);
createMap(
diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts
index 4dcf538aac..c61ecef6d8 100644
--- a/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts
+++ b/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts
@@ -38,6 +38,8 @@ import { NoticeOfIntent } from '../../alcs/notice-of-intent/notice-of-intent.ent
import { NoticeOfIntentPortalDecisionDto } from '../../portal/public/notice-of-intent/notice-of-intent-decision.dto';
import { NoticeOfIntentDecisionConditionDate } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-date/notice-of-intent-decision-condition-date.entity';
import { NoticeOfIntentDecisionConditionDateDto } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-date/notice-of-intent-decision-condition-date.dto';
+import { User } from '../../user/user.entity';
+import { UserDto } from '../../user/user.dto';
@Injectable()
export class NoticeOfIntentDecisionProfile extends AutomapperProfile {
@@ -115,6 +117,22 @@ export class NoticeOfIntentDecisionProfile extends AutomapperProfile {
}
}),
),
+ forMember(
+ (ad) => ad.followUpAt,
+ mapFrom((a) => a.followUpAt?.getTime()),
+ ),
+ forMember(
+ (ad) => ad.flaggedBy,
+ mapFrom((a) => (a.flaggedBy ? this.mapper.map(a.flaggedBy, User, UserDto) : a.flaggedBy)),
+ ),
+ forMember(
+ (ad) => ad.flagEditedBy,
+ mapFrom((a) => (a.flagEditedBy ? this.mapper.map(a.flagEditedBy, User, UserDto) : a.flagEditedBy)),
+ ),
+ forMember(
+ (ad) => ad.flagEditedAt,
+ mapFrom((a) => a.flagEditedAt?.getTime()),
+ ),
);
createMap(mapper, NoticeOfIntentSubmissionStatusType, NoticeOfIntentStatusDto);
diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1737148712969-add_flag_fields_to_app_noi_decisions.ts b/services/apps/alcs/src/providers/typeorm/migrations/1737148712969-add_flag_fields_to_app_noi_decisions.ts
new file mode 100644
index 0000000000..e1199a24e4
--- /dev/null
+++ b/services/apps/alcs/src/providers/typeorm/migrations/1737148712969-add_flag_fields_to_app_noi_decisions.ts
@@ -0,0 +1,44 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class AddFlagFieldsToAppNoiDecisions1737148712969 implements MigrationInterface {
+ name = 'AddFlagFieldsToAppNoiDecisions1737148712969'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" ADD "is_flagged" boolean NOT NULL DEFAULT false`);
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" ADD "reason_flagged" text`);
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" ADD "follow_up_at" TIMESTAMP WITH TIME ZONE`);
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" ADD "flag_edited_at" TIMESTAMP WITH TIME ZONE`);
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" ADD "flagged_by_uuid" uuid`);
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" ADD "flag_edited_by_uuid" uuid`);
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" ADD "is_flagged" boolean NOT NULL DEFAULT false`);
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" ADD "reason_flagged" text`);
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" ADD "follow_up_at" TIMESTAMP WITH TIME ZONE`);
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" ADD "flag_edited_at" TIMESTAMP WITH TIME ZONE`);
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" ADD "flagged_by_uuid" uuid`);
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" ADD "flag_edited_by_uuid" uuid`);
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" ADD CONSTRAINT "FK_ba6fa8d1851029a9859afc35b03" FOREIGN KEY ("flagged_by_uuid") REFERENCES "alcs"."user"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`);
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" ADD CONSTRAINT "FK_93cde17558333a6f39d089928de" FOREIGN KEY ("flag_edited_by_uuid") REFERENCES "alcs"."user"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`);
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" ADD CONSTRAINT "FK_9b50f52d4c843ff2656ce04e575" FOREIGN KEY ("flagged_by_uuid") REFERENCES "alcs"."user"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`);
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" ADD CONSTRAINT "FK_8b3ab9ae1ef21da9ebe8b358a39" FOREIGN KEY ("flag_edited_by_uuid") REFERENCES "alcs"."user"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" DROP CONSTRAINT "FK_8b3ab9ae1ef21da9ebe8b358a39"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" DROP CONSTRAINT "FK_9b50f52d4c843ff2656ce04e575"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" DROP CONSTRAINT "FK_93cde17558333a6f39d089928de"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" DROP CONSTRAINT "FK_ba6fa8d1851029a9859afc35b03"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" DROP COLUMN "flag_edited_by_uuid"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" DROP COLUMN "flagged_by_uuid"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" DROP COLUMN "flag_edited_at"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" DROP COLUMN "follow_up_at"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" DROP COLUMN "reason_flagged"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."notice_of_intent_decision" DROP COLUMN "is_flagged"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" DROP COLUMN "flag_edited_by_uuid"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" DROP COLUMN "flagged_by_uuid"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" DROP COLUMN "flag_edited_at"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" DROP COLUMN "follow_up_at"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" DROP COLUMN "reason_flagged"`);
+ await queryRunner.query(`ALTER TABLE "alcs"."application_decision" DROP COLUMN "is_flagged"`);
+ }
+
+}
diff --git a/services/apps/alcs/src/utils/incoming-date.formatter.ts b/services/apps/alcs/src/utils/incoming-date.formatter.ts
index 0d74ab7c25..6ef039558e 100644
--- a/services/apps/alcs/src/utils/incoming-date.formatter.ts
+++ b/services/apps/alcs/src/utils/incoming-date.formatter.ts
@@ -1,5 +1,5 @@
export const formatIncomingDate = (date?: number | null) => {
- if (date) {
+ if (date !== undefined && date !== null) {
return new Date(date);
} else if (date === null) {
return null;