Skip to content

Commit

Permalink
feat: #448 Add Timber Sales Manager Field (#485)
Browse files Browse the repository at this point in the history
* Add migration for new project.bcts_manager_name column. Add new field to create/response objects.

* Add new spec for swagger. Generated api-client.

* Add new bctsMgrName property to project entity.

* Fix test issue.

* Add bctsMgrName for admin form and necessary annotation(validation)

* Add Timber Sales Manager Name field and trigger update when forest client selection is changed.

* Add correct validation annotation for bctsMgrName.

* Admin view bctsMgrName field.

* Add validateTimberSalesManager for project service.

* Add condtion to display bctsMgrName.

* Fix indentation issue.

* Add display Timber Sales manager name to admin summary page.

* Adjust comment for Timber Sales string check condition.
  • Loading branch information
ianliuwk1019 authored Nov 7, 2023
1 parent 39de058 commit a21c46b
Show file tree
Hide file tree
Showing 17 changed files with 179 additions and 16 deletions.
48 changes: 45 additions & 3 deletions admin/src/app/foms/fom-add-edit/fom-add-edit.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,17 @@ <h1 class="text-muted">{{isCreate ? 'Add New' : 'Edit'}} FOM {{isCreate ? '' : '
</div>
<div class="form-group col">
<label>FOM Holder</label>
<select *ngIf="this.isCreate" class="sort-comments form-control" [ngClass]="{'is-invalid': !forestClientSelect && isSubmitSaveClicked}" (change)="changeForestClientId($event)" id="forestClient-list">
<select *ngIf="this.isCreate"
class="sort-comments form-control"
formControlName="forestClient"
[ngClass]="{'is-invalid': !forestClientSelect && isSubmitSaveClicked}"
(change)="onForestClientChange($event)"
id="forestClient-list">
<option *ngIf="this.isCreate"> </option>
<option [value]="client.id" *ngFor="let client of forestClients" [selected]="client.id === forestClientSelect" [innerHTML]=" client.name"> </option>
<option *ngFor="let client of forestClients"
[ngValue]="client"
[selected]="client.id === forestClientSelect"
[innerHTML]=" client.name"> </option>
</select>
<input *ngIf="!this.isCreate"
type="text"
Expand All @@ -231,7 +239,41 @@ <h1 class="text-muted">{{isCreate ? 'Add New' : 'Edit'}} FOM {{isCreate ? '' : '
&& getErrorMessage('forestClient', 'required')">
{{getErrorMessage('forestClient', 'required')}}
</div>
</div>
</div>
</div>
<div class="row">
<div class="form-group col">
<label [ngClass]="{'required': isHolderBctsManger()}">
Timber Sales Manager Name
</label>
<input
type="text"
class="form-control"
id="mgr-name-id"
name="bctsMgrName"
maxlength="50"
formControlName="bctsMgrName"
[appFormControl]="fg.get('bctsMgrName')"
/>
<div class="invalid-feedback"
*ngIf="(isSubmitSaveClicked || fieldTouchedOrDirty('bctsMgrName'))
&& getErrorMessage('bctsMgrName', 'required')">
{{getErrorMessage('bctsMgrName', 'required')}}
</div>
<div class="invalid-feedback"
*ngIf="(isSubmitSaveClicked || fieldTouchedOrDirty('bctsMgrName'))
&& getErrorMessage('bctsMgrName', 'minLength')">
{{getErrorMessage('bctsMgrName', 'minLength')}}
</div>
<div class="invalid-feedback"
*ngIf="(isSubmitSaveClicked || fieldTouchedOrDirty('bctsMgrName'))
&& getErrorMessage('bctsMgrName', 'maxLength')">
{{getErrorMessage('bctsMgrName', 'maxLength')}}
</div>
</div>



</div>
<div class="row">
<div class="form-group col-md">
Expand Down
21 changes: 17 additions & 4 deletions admin/src/app/foms/fom-add-edit/fom-add-edit.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
17 changes: 14 additions & 3 deletions admin/src/app/foms/fom-add-edit/fom-add-edit.form.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -11,7 +11,8 @@ const updateFields = [
'fspId',
'district',
'forestClient',
'workflowState'
'workflowState',
'bctsMgrName'
] as const;

export class FomAddEditForm implements Pick<ProjectResponse,
Expand Down Expand Up @@ -44,7 +45,6 @@ export class FomAddEditForm implements Pick<ProjectResponse,

@prop()
@required({message: 'FOM Holder is required.'})
@minLength({value: 1})
forestClient: ForestClientResponse;

@prop()
Expand All @@ -61,6 +61,17 @@ export class FomAddEditForm implements Pick<ProjectResponse,
@prop()
opEndDate: Date;

@prop()
@required({
conditionalExpression: x => {
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<ProjectResponse>) {
if (project) {

Expand Down
7 changes: 7 additions & 0 deletions admin/src/app/foms/fom-detail/fom-detail.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ <h2>Details</h2>
{{project.forestClient.name}}
</span>
</li>
<li *ngIf="project.bctsMgrName && project.bctsMgrName.length > 0">
<span class="name">Timber Sales Manager Name:</span>
<span
class="value">
{{project.bctsMgrName}}
</span>
</li>
<li>
<span class="name">District:</span>
<span
Expand Down
2 changes: 1 addition & 1 deletion admin/src/app/foms/fom-detail/fom-detail.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ section {
}

span.name {
@include flex(0 0 12rem);
@include flex(0 0 13rem);

margin: 0;
}
Expand Down
7 changes: 7 additions & 0 deletions admin/src/app/foms/summary/summary.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ <h4>Details</h4>
{{project?.forestClient?.name}}
</span>
</li>
<li *ngIf="project.bctsMgrName && project.bctsMgrName.length > 0">
<span class="name">Timber Sales Manager Name:</span>
<span
class="value">
{{project.bctsMgrName}}
</span>
</li>
<li>
<span class="name">District:</span>
<span class="value">
Expand Down
2 changes: 1 addition & 1 deletion admin/src/app/foms/summary/summary.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
}

span.name {
@include flex(0 0 12rem);
@include flex(0 0 13rem);
margin: 0;
}

Expand Down
15 changes: 13 additions & 2 deletions api/openapi/swagger-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -1839,6 +1839,9 @@
},
"operationEndYear": {
"type": "number"
},
"bctsMgrName": {
"type": "string"
}
},
"required": [
Expand All @@ -1858,7 +1861,8 @@
"publicNoticeId",
"noticePostDate",
"operationStartYear",
"operationEndYear"
"operationEndYear",
"bctsMgrName"
]
},
"ProjectMetricsResponse": {
Expand Down Expand Up @@ -1915,6 +1919,9 @@
},
"operationEndYear": {
"type": "number"
},
"bctsMgrName": {
"type": "string"
}
},
"required": [
Expand All @@ -1926,7 +1933,8 @@
"fspId",
"districtId",
"operationStartYear",
"operationEndYear"
"operationEndYear",
"bctsMgrName"
]
},
"ProjectUpdateRequest": {
Expand Down Expand Up @@ -1958,6 +1966,9 @@
"operationEndYear": {
"type": "number"
},
"bctsMgrName": {
"type": "string"
},
"revisionCount": {
"type": "number"
}
Expand Down
10 changes: 9 additions & 1 deletion api/src/app/modules/project/project.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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']) {
Expand Down Expand Up @@ -147,6 +152,9 @@ export class ProjectResponse {

@ApiProperty({ required: true })
operationEndYear: number;

@ApiProperty()
bctsMgrName: string;
}

export class ProjectMetricsResponse {
Expand Down
2 changes: 2 additions & 0 deletions api/src/app/modules/project/project.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,6 @@ export class Project extends ApiBaseEntity<Project> {
@Column({ name: 'operation_end_year'})
operationEndYear: number;

@Column({ name: 'bcts_manager_name'})
bctsMgrName: string;
}
28 changes: 28 additions & 0 deletions api/src/app/modules/project/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,36 @@ export class ProjectService extends DataService<Project, Repository<Project>, Pr
async create(request: any, user: User): Promise<ProjectResponse> {
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<ProjectResponse> {
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<ProjectResponse[]> {
this.logger.debug('Find criteria: %o', findCriteria);

Expand Down Expand Up @@ -231,6 +258,7 @@ export class ProjectService extends DataService<Project, Repository<Project>, Pr
}
response.operationStartYear = entity.operationStartYear;
response.operationEndYear = entity.operationEndYear;
response.bctsMgrName = entity.bctsMgrName;
return response;
}

Expand Down
3 changes: 2 additions & 1 deletion api/src/app/modules/project/public-notice.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,8 @@ function getSimpleProjectResponseData() {
"commentClassificationMandatory": false,
"publicNoticeId": 10001,
"operationStartYear": 2028,
"operationEndYear": 2031
"operationEndYear": 2031,
"bctsMgrName": null
}
return data;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
`);
}

}
1 change: 1 addition & 0 deletions libs/client/typescript-ng/model/projectCreateRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ export interface ProjectCreateRequest {
districtId: number;
operationStartYear: number;
operationEndYear: number;
bctsMgrName: string;
}

1 change: 1 addition & 0 deletions libs/client/typescript-ng/model/projectResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,6 @@ export interface ProjectResponse {
noticePostDate: string;
operationStartYear: number;
operationEndYear: number;
bctsMgrName: string;
}

1 change: 1 addition & 0 deletions libs/client/typescript-ng/model/projectUpdateRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface ProjectUpdateRequest {
districtId?: number;
operationStartYear?: number;
operationEndYear?: number;
bctsMgrName?: string;
revisionCount: number;
}

Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ <h3>FOM Details</h3>
<li>
FOM Holder:&nbsp;&nbsp;{{project.forestClient.name | titlecase}}
</li>
<li *ngIf="project.bctsMgrName && project.bctsMgrName.length > 0">
Timber Sales Manager Name:&nbsp;&nbsp;{{project.bctsMgrName | titlecase}}
</li>
<li>
<strong>FSP ID:&nbsp;&nbsp;{{project.fspId}}</strong>
</li>
Expand Down

0 comments on commit a21c46b

Please sign in to comment.