diff --git a/admin/src/app/foms/fom-add-edit/fom-add-edit.component.html b/admin/src/app/foms/fom-add-edit/fom-add-edit.component.html index 66d6911e6..f5f2b744f 100644 --- a/admin/src/app/foms/fom-add-edit/fom-add-edit.component.html +++ b/admin/src/app/foms/fom-add-edit/fom-add-edit.component.html @@ -214,9 +214,17 @@

{{isCreate ? 'Add New' : 'Edit'}} FOM {{isCreate ? '' : '
- - + {{isCreate ? 'Add New' : 'Edit'}} FOM {{isCreate ? '' : ' && getErrorMessage('forestClient', 'required')"> {{getErrorMessage('forestClient', 'required')}}
- + + +
+
+ + +
+ {{getErrorMessage('bctsMgrName', 'required')}} +
+
+ {{getErrorMessage('bctsMgrName', 'minLength')}} +
+
+ {{getErrorMessage('bctsMgrName', 'maxLength')}} +
+
+ + +
diff --git a/admin/src/app/foms/fom-add-edit/fom-add-edit.component.ts b/admin/src/app/foms/fom-add-edit/fom-add-edit.component.ts index 76414d81e..30f1cab77 100644 --- a/admin/src/app/foms/fom-add-edit/fom-add-edit.component.ts +++ b/admin/src/app/foms/fom-add-edit/fom-add-edit.component.ts @@ -249,7 +249,7 @@ export class FomAddEditComponent implements OnInit, AfterViewInit, OnDestroy { if (this.stateSvc.loading) return; let projectCreate = this.fg.value as ProjectCreateRequest projectCreate['districtId'] = this.districtIdSelect; - projectCreate.forestClientNumber = this.fg.get('forestClient').value; + projectCreate.forestClientNumber = this.fg.get('forestClient').value.id; const cmoDateVal = this.fg.get('commentingOpenDate').value; const cmcDateVal = this.fg.get('commentingClosedDate').value; projectCreate.commentingOpenDate = cmoDateVal? moment(cmoDateVal).format('YYYY-MM-DD'): null; @@ -318,9 +318,22 @@ export class FomAddEditComponent implements OnInit, AfterViewInit, OnDestroy { this.districtIdSelect = parseInt(e.target.value); } - changeForestClientId(e) { - this.fg.get('forestClient').setValue(e.target.value); - this.forestClientSelect = parseInt(e.target.value); + onForestClientChange(e) { + const forestClientField = this.fg.get('forestClient'); + this.fg.get('forestClient').setValue(forestClientField.value); + this.forestClientSelect = parseInt(forestClientField.value.id); + + // 'TIMBER SALES MANAGER' name field is required (to be validated) based on forestClient name + // conditionally. Due to it's validation is annotation-based in fom-add-edit.form.ts + // (using @rxweb/reactive-form-validators), when forestClient is changed, bctsMgrName does not get + // rerenderred (no ngIf, ngFor etc on this field). + // Just trigger the dynamic form field (with enable()) is probably easier than using 'ChangeDetectorRef'. + this.fg.get('bctsMgrName').enable(); + } + + isHolderBctsManger() { + const forestClientField = this.fg.get('forestClient'); + return forestClientField.value?.name?.toUpperCase().includes('TIMBER SALES MANAGER'); } changeDescription(e) { diff --git a/admin/src/app/foms/fom-add-edit/fom-add-edit.form.ts b/admin/src/app/foms/fom-add-edit/fom-add-edit.form.ts index bd7653bbd..c08907375 100644 --- a/admin/src/app/foms/fom-add-edit/fom-add-edit.form.ts +++ b/admin/src/app/foms/fom-add-edit/fom-add-edit.form.ts @@ -1,5 +1,5 @@ import { DistrictResponse, ForestClientResponse, ProjectResponse, WorkflowStateCode } from '@api-client'; -import { minLength, prop, required, minDate } from '@rxweb/reactive-form-validators'; +import { minLength, prop, required, minDate, maxLength } from '@rxweb/reactive-form-validators'; import moment = require("moment"); import * as R from 'remeda'; @@ -11,7 +11,8 @@ const updateFields = [ 'fspId', 'district', 'forestClient', - 'workflowState' + 'workflowState', + 'bctsMgrName' ] as const; export class FomAddEditForm implements Pick { + return x.forestClient?.name.toUpperCase().includes('TIMBER SALES MANAGER') + }, + message: 'Timber Sales Manager Name is required.', + }) + @minLength({value: 3, message: 'Minimum length is 3'}) + @maxLength({value: 50, message: 'Maximum length is 50'}) + bctsMgrName: string; + constructor(project?: Partial) { if (project) { diff --git a/admin/src/app/foms/fom-detail/fom-detail.component.html b/admin/src/app/foms/fom-detail/fom-detail.component.html index 37b43a359..eaf6052e5 100644 --- a/admin/src/app/foms/fom-detail/fom-detail.component.html +++ b/admin/src/app/foms/fom-detail/fom-detail.component.html @@ -103,6 +103,13 @@

Details

{{project.forestClient.name}} +
  • + Timber Sales Manager Name: + + {{project.bctsMgrName}} + +
  • District: Details
  • {{project?.forestClient?.name}} +
  • + Timber Sales Manager Name: + + {{project.bctsMgrName}} + +
  • District: diff --git a/admin/src/app/foms/summary/summary.component.scss b/admin/src/app/foms/summary/summary.component.scss index 4853cd2cf..9e83708f6 100644 --- a/admin/src/app/foms/summary/summary.component.scss +++ b/admin/src/app/foms/summary/summary.component.scss @@ -75,7 +75,7 @@ } span.name { - @include flex(0 0 12rem); + @include flex(0 0 13rem); margin: 0; } diff --git a/api/openapi/swagger-spec.json b/api/openapi/swagger-spec.json index fb7896db7..c228426a7 100644 --- a/api/openapi/swagger-spec.json +++ b/api/openapi/swagger-spec.json @@ -1839,6 +1839,9 @@ }, "operationEndYear": { "type": "number" + }, + "bctsMgrName": { + "type": "string" } }, "required": [ @@ -1858,7 +1861,8 @@ "publicNoticeId", "noticePostDate", "operationStartYear", - "operationEndYear" + "operationEndYear", + "bctsMgrName" ] }, "ProjectMetricsResponse": { @@ -1915,6 +1919,9 @@ }, "operationEndYear": { "type": "number" + }, + "bctsMgrName": { + "type": "string" } }, "required": [ @@ -1926,7 +1933,8 @@ "fspId", "districtId", "operationStartYear", - "operationEndYear" + "operationEndYear", + "bctsMgrName" ] }, "ProjectUpdateRequest": { @@ -1958,6 +1966,9 @@ "operationEndYear": { "type": "number" }, + "bctsMgrName": { + "type": "string" + }, "revisionCount": { "type": "number" } diff --git a/api/src/app/modules/project/project.dto.ts b/api/src/app/modules/project/project.dto.ts index 01d4e7c5a..c9587444a 100644 --- a/api/src/app/modules/project/project.dto.ts +++ b/api/src/app/modules/project/project.dto.ts @@ -5,7 +5,7 @@ import { Point } from 'geojson'; import { DistrictResponse } from '../district/district.dto'; import { ForestClientResponse } from '../forest-client/forest-client.dto'; import { IsBoolean, IsDateString, IsEnum, IsNumber, IsNumberString, IsOptional, MaxLength, - MinLength, Min, registerDecorator, ValidationArguments, ValidationOptions } from 'class-validator'; + MinLength, Min, registerDecorator, ValidationArguments, ValidationOptions, IsNotEmpty, ValidateIf } from 'class-validator'; export class ProjectCreateRequest { @ApiProperty() @@ -52,6 +52,11 @@ export class ProjectCreateRequest { }) operationEndYear: number; + @ApiProperty() + @ValidateIf(o => o.bctsMgrName && o.bctsMgrName.length > 0) // validate when not empty. + @MinLength(3) + @MaxLength(50) + bctsMgrName?: string; } export class ProjectUpdateRequest extends OmitType(PartialType(ProjectCreateRequest), ['forestClientNumber']) { @@ -147,6 +152,9 @@ export class ProjectResponse { @ApiProperty({ required: true }) operationEndYear: number; + + @ApiProperty() + bctsMgrName: string; } export class ProjectMetricsResponse { diff --git a/api/src/app/modules/project/project.entity.ts b/api/src/app/modules/project/project.entity.ts index e97794bcd..57b1ba350 100644 --- a/api/src/app/modules/project/project.entity.ts +++ b/api/src/app/modules/project/project.entity.ts @@ -73,4 +73,6 @@ export class Project extends ApiBaseEntity { @Column({ name: 'operation_end_year'}) operationEndYear: number; + @Column({ name: 'bcts_manager_name'}) + bctsMgrName: string; } diff --git a/api/src/app/modules/project/project.service.ts b/api/src/app/modules/project/project.service.ts index dd266d95a..71546c8ae 100644 --- a/api/src/app/modules/project/project.service.ts +++ b/api/src/app/modules/project/project.service.ts @@ -169,9 +169,36 @@ export class ProjectService extends DataService, Pr async create(request: any, user: User): Promise { request.workflowStateCode = WorkflowStateEnum.INITIAL; request.forestClientId = request.forestClientNumber; + await this.validateTimberSalesManager(request.bctsMgrName, request.forestClientNumber, null); return super.create(request, user); } + async update(projectId: number, request: any, user: User): Promise { + await this.validateTimberSalesManager(request.bctsMgrName, null, projectId); + return super.update(projectId, request, user); + } + + async validateTimberSalesManager(bctsMgrName: string, forestClientNumber?: string, projectId?: number) { + let forestClientName = null; + if (projectId) { + const projectEntity = await this.findEntityWithCommonRelations(projectId) + forestClientName = projectEntity.forestClient.name; + } + else { + const fcl = await this.forestClientService.find([forestClientNumber]); + if (fcl && fcl.length == 1) { + forestClientName = fcl[0].name; + } + } + + // Timber Manager's name is required when the holder (foreset client name) contains "TIMBER SALES MANAGER" + if (forestClientName && forestClientName.toUpperCase().includes('TIMBER SALES MANAGER')) { + if (!bctsMgrName || bctsMgrName.length == 0) { + throw new BadRequestException("Timber Sales Manager name is required for Timber Sales FOM."); + } + } + } + async find(findCriteria: ProjectFindCriteria):Promise { this.logger.debug('Find criteria: %o', findCriteria); @@ -231,6 +258,7 @@ export class ProjectService extends DataService, Pr } response.operationStartYear = entity.operationStartYear; response.operationEndYear = entity.operationEndYear; + response.bctsMgrName = entity.bctsMgrName; return response; } diff --git a/api/src/app/modules/project/public-notice.service.spec.ts b/api/src/app/modules/project/public-notice.service.spec.ts index 5722a0eb0..94e46fcf8 100644 --- a/api/src/app/modules/project/public-notice.service.spec.ts +++ b/api/src/app/modules/project/public-notice.service.spec.ts @@ -299,7 +299,8 @@ function getSimpleProjectResponseData() { "commentClassificationMandatory": false, "publicNoticeId": 10001, "operationStartYear": 2028, - "operationEndYear": 2031 + "operationEndYear": 2031, + "bctsMgrName": null } return data; } diff --git a/api/src/migrations/main/1698445357799-project-addColumn-bctsManagerName.js b/api/src/migrations/main/1698445357799-project-addColumn-bctsManagerName.js new file mode 100644 index 000000000..f94e3d7eb --- /dev/null +++ b/api/src/migrations/main/1698445357799-project-addColumn-bctsManagerName.js @@ -0,0 +1,27 @@ +const { MigrationInterface, QueryRunner } = require("typeorm"); + +module.exports = class ProjectAddColumnBctsManagerName1698445357799 { + + async up(queryRunner) { + console.log('Starting project (add new column - bcts_manager_name) migration'); + await queryRunner.query(` + + -- add new column - bcts_manager_name + ALTER TABLE app_fom.project ADD COLUMN bcts_manager_name varchar; + + -- comment on column - bcts_manager_name + COMMENT ON COLUMN app_fom.project.bcts_manager_name IS + 'BCTS timber sales manager''s name when it is a BCTS FOM.'; + `); + } + + async down(queryRunner) { + console.log('Starting project (drop column - bcts_manager_name) migration'); + await queryRunner.query(` + + -- drop new column - bcts_manager_name + ALTER TABLE app_fom.project DROP COLUMN bcts_manager_name; + `); + } + +} diff --git a/libs/client/typescript-ng/model/projectCreateRequest.ts b/libs/client/typescript-ng/model/projectCreateRequest.ts index 0bb3c4a19..8e49e8572 100644 --- a/libs/client/typescript-ng/model/projectCreateRequest.ts +++ b/libs/client/typescript-ng/model/projectCreateRequest.ts @@ -27,5 +27,6 @@ export interface ProjectCreateRequest { districtId: number; operationStartYear: number; operationEndYear: number; + bctsMgrName: string; } diff --git a/libs/client/typescript-ng/model/projectResponse.ts b/libs/client/typescript-ng/model/projectResponse.ts index c2ad7dbef..42af8d838 100644 --- a/libs/client/typescript-ng/model/projectResponse.ts +++ b/libs/client/typescript-ng/model/projectResponse.ts @@ -44,5 +44,6 @@ export interface ProjectResponse { noticePostDate: string; operationStartYear: number; operationEndYear: number; + bctsMgrName: string; } diff --git a/libs/client/typescript-ng/model/projectUpdateRequest.ts b/libs/client/typescript-ng/model/projectUpdateRequest.ts index 51974e695..29323aae6 100644 --- a/libs/client/typescript-ng/model/projectUpdateRequest.ts +++ b/libs/client/typescript-ng/model/projectUpdateRequest.ts @@ -26,6 +26,7 @@ export interface ProjectUpdateRequest { districtId?: number; operationStartYear?: number; operationEndYear?: number; + bctsMgrName?: string; revisionCount: number; } diff --git a/public/src/app/applications/details-panel/details-panel.component.html b/public/src/app/applications/details-panel/details-panel.component.html index 2291b363c..b23ece48d 100644 --- a/public/src/app/applications/details-panel/details-panel.component.html +++ b/public/src/app/applications/details-panel/details-panel.component.html @@ -55,6 +55,9 @@

    FOM Details

  • FOM Holder:  {{project.forestClient.name | titlecase}}
  • +
  • + Timber Sales Manager Name:  {{project.bctsMgrName | titlecase}} +
  • FSP ID:  {{project.fspId}}