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 }}
+ + +
+
+
+ {{ decision.flaggedBy?.prettyName }} +
+
Follow-Up Date: {{ formatDate(decision.followUpAt) || 'No Data' }}
+
+ +
+ 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')" > + +
diff --git a/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.scss b/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.scss index 6b3c5c20e5..bafd67ffb7 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.scss +++ b/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.scss @@ -104,6 +104,7 @@ .pill-row { display: flex; flex-direction: row; + align-items: flex-end; gap: 1px; } @@ -111,3 +112,7 @@ color: colors.$grey; font-weight: 400; } + +.flag-icon { + color: blue; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html index ec3f627154..23daa357ec 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html @@ -75,8 +75,24 @@
Res #{{ decision.resolutionNumber }}/{{ decision.resolutionYear }}
+ +
+ +
+ - +
Resolution

+ + +
+
+
+ {{ decision.flaggedBy?.prettyName }} +
+
Follow-Up Date: {{ formatDate(decision.followUpAt) || 'No Data' }}
+
+ +
+ 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: + + + + + + + +
+ + +
+
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;