diff --git a/.gitignore b/.gitignore index c82deacaa5..b203e41dac 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,4 @@ playwright-report/ playwright/.cache/ .~lock.* -/ora2pg_data \ No newline at end of file +/ora2pg_data diff --git a/alcs-frontend/src/app/features/admin/admin.component.ts b/alcs-frontend/src/app/features/admin/admin.component.ts index b5d4035ce3..b2500c699c 100644 --- a/alcs-frontend/src/app/features/admin/admin.component.ts +++ b/alcs-frontend/src/app/features/admin/admin.component.ts @@ -10,6 +10,7 @@ import { HolidayComponent } from './holiday/holiday.component'; import { LocalGovernmentComponent } from './local-government/local-government.component'; import { NoiSubtypeComponent } from './noi-subtype/noi-subtype.component'; import { UnarchiveComponent } from './unarchive/unarchive.component'; +import { TagContainerComponent } from './tag/tag-container.component'; export const childRoutes = [ { @@ -72,6 +73,12 @@ export const childRoutes = [ icon: 'settings_applications', component: ConfigurationComponent, }, + { + path: 'tag', + menuTitle: 'Tags/Categories', + icon: 'sell', + component: TagContainerComponent, + }, ]; @Component({ diff --git a/alcs-frontend/src/app/features/admin/admin.module.ts b/alcs-frontend/src/app/features/admin/admin.module.ts index 30a5b452e1..716b5b8e94 100644 --- a/alcs-frontend/src/app/features/admin/admin.module.ts +++ b/alcs-frontend/src/app/features/admin/admin.module.ts @@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { MatChipsModule } from '@angular/material/chips'; import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatTabsModule } from '@angular/material/tabs'; import { RouterModule, Routes } from '@angular/router'; import { SharedModule } from '../../shared/shared.module'; import { AdminComponent, childRoutes } from './admin.component'; @@ -25,6 +26,11 @@ import { LocalGovernmentComponent } from './local-government/local-government.co import { NoiSubtypeDialogComponent } from './noi-subtype/noi-subtype-dialog/noi-subtype-dialog.component'; import { NoiSubtypeComponent } from './noi-subtype/noi-subtype.component'; import { UnarchiveComponent } from './unarchive/unarchive.component'; +import { TagCategoryComponent } from './tag/tag-category/tag-category.component'; +import { TagCategoryDialogComponent } from './tag/tag-category/tag-category-dialog/tag-category-dialog.component'; +import { TagComponent } from './tag/tag.component'; +import { TagDialogComponent } from './tag/tag-dialog/tag-dialog.component'; +import { TagContainerComponent } from './tag/tag-container.component'; const routes: Routes = [ { @@ -56,6 +62,11 @@ const routes: Routes = [ BoardManagementDialogComponent, ConfigurationComponent, MaintenanceBannerConfirmationDialogComponent, + TagCategoryComponent, + TagCategoryDialogComponent, + TagComponent, + TagDialogComponent, + TagContainerComponent, ], imports: [ CommonModule, @@ -64,6 +75,7 @@ const routes: Routes = [ MatPaginatorModule, DragDropModule, MatChipsModule, + MatTabsModule, ], }) export class AdminModule {} diff --git a/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.html b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.html new file mode 100644 index 0000000000..f4df4b696a --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.html @@ -0,0 +1,36 @@ +
+

{{ isEdit ? 'Edit' : 'Create New' }} Category

+
+
+
+ +
+ + Name + + +
+
+ info Warning:   Category already exists. Choose a different category name. +
+
+
+ +
+ + +
+ + +
+
+
diff --git a/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.scss b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.scss new file mode 100644 index 0000000000..6483caf822 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.scss @@ -0,0 +1,55 @@ +@use '../../../../../../styles/colors'; + +mat-dialog-title { + margin-bottom: 24px; +} + +.warning-banner { + margin-top: 8px; + background-color: green; + padding: 16px; + + .display-none { + display: none !important; + } +} + +.warning { + padding: 16px; + font-size: 14px; + background-color: rgba(colors.$field-warning-bg-color, 0.5); + border-radius: 8px; + display: flex; + align-items: center; + color: colors.$dark-contrast-text; + margin-top: 8px; + + mat-icon { + color: colors.$dark-contrast-text; + margin-right: 16px; + } +} + +.dialog { + padding: 24px; + + form { + display: grid; + grid-template-columns: 1fr 1fr; + row-gap: 24px; + column-gap: 24px; + margin-bottom: 12px; + + .description { + margin-top: 20px; + } + + .full-width { + grid-column: 1/3; + } + + .mat-mdc-form-field { + width: 100%; + } + } +} \ No newline at end of file diff --git a/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.spec.ts b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.spec.ts new file mode 100644 index 0000000000..caf7a192f6 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.spec.ts @@ -0,0 +1,36 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { TagCategoryService } from '../../../../../services/tag/tag-category/tag-category.service'; + +import { TagCategoryDialogComponent } from './tag-category-dialog.component'; + +describe('TagCategoryDialogComponent', () => { + let component: TagCategoryDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, FormsModule], + declarations: [TagCategoryDialogComponent], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: undefined }, + { provide: MatDialogRef, useValue: {} }, + { + provide: TagCategoryService, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TagCategoryDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.ts b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.ts new file mode 100644 index 0000000000..8ade8f8cb9 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category-dialog/tag-category-dialog.component.ts @@ -0,0 +1,70 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { TagCategoryDto } from '../../../../../services/tag/tag-category/tag-category.dto'; +import { TagCategoryService } from '../../../../../services/tag/tag-category/tag-category.service'; +import { FormControl } from '@angular/forms'; + +@Component({ + selector: 'app-tag-category-dialog', + templateUrl: './tag-category-dialog.component.html', + styleUrls: ['./tag-category-dialog.component.scss'], +}) +export class TagCategoryDialogComponent { + name = ''; + uuid = ''; + + isLoading = false; + isEdit = false; + showNameWarning = false; + nameControl = new FormControl(); + + constructor( + @Inject(MAT_DIALOG_DATA) public data: TagCategoryDto | undefined, + private dialogRef: MatDialogRef, + private tagCategoryService: TagCategoryService, + ) { + if (data) { + this.uuid = data.uuid; + this.name = data.name; + } + this.isEdit = !!data; + } + + async onSubmit() { + this.isLoading = true; + + const dto: TagCategoryDto = { + uuid: this.uuid, + name: this.name, + }; + + if (this.isEdit) { + try { + await this.tagCategoryService.update(this.uuid, dto); + } catch (e) { + this.showWarning(); + this.isLoading = false; + return; + } + } else { + try { + await this.tagCategoryService.create(dto); + } catch (e) { + this.showWarning(); + this.isLoading = false; + return; + } + } + this.isLoading = false; + this.dialogRef.close(true); + } + + onChange() { + this.showNameWarning = false; + } + + private showWarning() { + this.showNameWarning = true; + this.nameControl.setErrors({"invalid": true}); + } +} diff --git a/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category.component.html b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category.component.html new file mode 100644 index 0000000000..ddcb6db6d9 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category.component.html @@ -0,0 +1,58 @@ +
+
+
+
+ + Enter category + + + + {{option}} + + + +
+
+ +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + +
Number{{ row.number }}Name{{ row.name }}Actions + + +
+ + +
diff --git a/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category.component.scss b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category.component.scss new file mode 100644 index 0000000000..4e348f6571 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category.component.scss @@ -0,0 +1,48 @@ +@use '../../../../../styles/colors'; + +.container { + .actions-bar { + margin-top: 30px; + display: flex; + justify-content: space-between; + } + + .search-control { + margin-right: 30px; + display: inline-block; + } + + table { + margin: 35px 2px 30px 2px; + box-shadow: 2px 2px 8px 2px #00000040; + } + + .mdc-data-table__table { + min-width: unset!important; + width: 99%; + } + + .delete-btn { + color: colors.$error-color; + width: 48px!important; + min-width: unset!important; + } + + .mat-icon { + margin-right: 1px; + } + + .edit-btn { + color: colors.$primary-color-dark; + width: 48px!important; + min-width: unset!important; + } + + .mat-column-name { + width: 70%; + } + + .mat-column-actions { + width: 30%; + } +} diff --git a/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category.component.spec.ts b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category.component.spec.ts new file mode 100644 index 0000000000..528aed7daf --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category.component.spec.ts @@ -0,0 +1,53 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { NoiSubtypeService } from '../../../../services/noi-subtype/noi-subtype.service'; +import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; + +import { TagCategoryComponent } from './tag-category.component'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; + +fdescribe('TagCategoryComponent', () => { + let component: TagCategoryComponent; + let fixture: ComponentFixture; + let mockNoiSubtypeService: DeepMocked; + let mockDialog: DeepMocked; + let mockConfirmationDialogService: DeepMocked; + + beforeEach(async () => { + mockNoiSubtypeService = createMock(); + mockDialog = createMock(); + mockConfirmationDialogService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [TagCategoryComponent], + providers: [ + { + provide: NoiSubtypeService, + useValue: mockNoiSubtypeService, + }, + { + provide: MatDialog, + useValue: mockDialog, + }, + { + provide: ConfirmationDialogService, + useValue: mockConfirmationDialogService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + imports: [HttpClientTestingModule, MatAutocompleteModule], + }).compileComponents(); + + fixture = TestBed.createComponent(TagCategoryComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category.component.ts b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category.component.ts new file mode 100644 index 0000000000..d7bc036459 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag-category/tag-category.component.ts @@ -0,0 +1,111 @@ +import { Component, OnInit } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Subject, takeUntil } from 'rxjs'; +import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { TagCategoryDialogComponent } from './tag-category-dialog/tag-category-dialog.component'; +import { TagCategoryService } from '../../../../services/tag/tag-category/tag-category.service'; +import { TagCategoryDto } from '../../../../services/tag/tag-category/tag-category.dto'; +import { PageEvent } from '@angular/material/paginator'; +import { ToastService } from '../../../../services/toast/toast.service'; + +@Component({ + selector: 'app-tag-category', + templateUrl: './tag-category.component.html', + styleUrls: ['./tag-category.component.scss'], +}) +export class TagCategoryComponent implements OnInit { + destroy = new Subject(); + pageIndex = 0; + itemsPerPage = 20; + search?: string = undefined; + categories: TagCategoryDto[] = []; + total: number = 0; + displayedColumns: string[] = ['name', 'actions']; + filteredOptions: string[] = []; + + constructor( + private tagCategoryService: TagCategoryService, + public dialog: MatDialog, + private confirmationDialogService: ConfirmationDialogService, + private toastService: ToastService + ) {} + + ngOnInit(): void { + this.fetch(); + + this.tagCategoryService.$categories + .pipe(takeUntil(this.destroy)) + .subscribe((result: { data: TagCategoryDto[]; total: number }) => { + this.categories = result.data; + this.filteredOptions = []; + this.total = result.total; + }); + } + + ngOnDestroy(): void { + this.destroy.next(); + this.destroy.complete(); + } + + onPageChange($event: PageEvent) { + this.pageIndex = $event.pageIndex; + this.itemsPerPage = $event.pageSize; + + this.fetch(); + } + + async fetch() { + this.tagCategoryService.fetch(this.pageIndex, this.itemsPerPage, this.search); + } + + async onCreate() { + const dialog = this.dialog.open(TagCategoryDialogComponent, { + minWidth: '600px', + maxWidth: '800px', + width: '70%', + }); + dialog.beforeClosed().subscribe(async (result) => { + if (result) { + await this.fetch(); + } + }); + } + + async onEdit(categoryDto: TagCategoryDto) { + const dialog = this.dialog.open(TagCategoryDialogComponent, { + minWidth: '600px', + maxWidth: '800px', + width: '70%', + data: categoryDto, + }); + dialog.beforeClosed().subscribe(async (result) => { + if (result) { + await this.fetch(); + } + }); + } + + async onDelete(categoryDto: TagCategoryDto) { + this.confirmationDialogService + .openDialog({ + body: `Are you sure you want to delete ${categoryDto.name}?`, + }) + .subscribe(async (answer) => { + if (answer) { + try { + await this.tagCategoryService.delete(categoryDto.uuid); + } catch (e) { + this.toastService.showErrorToast('Category is associated with tags. Unable to delete.'); + } + await this.fetch(); + } + }); + } + + async updateFilter(value: string) { + const response = await this.tagCategoryService.search(0, 5, value); + if (response) { + this.filteredOptions = response.data.map((res) => res.name); + } + } +} diff --git a/alcs-frontend/src/app/features/admin/tag/tag-container.component.html b/alcs-frontend/src/app/features/admin/tag/tag-container.component.html new file mode 100644 index 0000000000..486a1495fe --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag-container.component.html @@ -0,0 +1,15 @@ + + + + Tags + + + + + + + Categories + + + + diff --git a/alcs-frontend/src/app/features/admin/tag/tag-container.component.scss b/alcs-frontend/src/app/features/admin/tag/tag-container.component.scss new file mode 100644 index 0000000000..f402b99d46 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag-container.component.scss @@ -0,0 +1,4 @@ +.tab-label { + font-weight: 700; + font-size: 16px; +} diff --git a/alcs-frontend/src/app/features/admin/tag/tag-container.component.ts b/alcs-frontend/src/app/features/admin/tag/tag-container.component.ts new file mode 100644 index 0000000000..07f7afb9ce --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag-container.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-tag-container', + templateUrl: './tag-container.component.html', + styleUrls: ['./tag-container.component.scss'], +}) +export class TagContainerComponent {} diff --git a/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-category-dialog.component.spec.ts b/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-category-dialog.component.spec.ts new file mode 100644 index 0000000000..e88c404a8c --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-category-dialog.component.spec.ts @@ -0,0 +1,48 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { AdminLocalGovernmentService } from '../../../../services/admin-local-government/admin-local-government.service'; + +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { PaginatedTagResponse, TagService } from '../../../../services/tag/tag.service'; +import { TagDialogComponent } from './tag-dialog.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +describe('TagDialogComponent', () => { + let component: TagDialogComponent; + let fixture: ComponentFixture; + let mockTagService: DeepMocked; + + beforeEach(async () => { + mockTagService = createMock(); + mockTagService.$tags = new BehaviorSubject({ data: [], total: 0 }); + + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, FormsModule, HttpClientTestingModule], + declarations: [TagDialogComponent], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: undefined }, + { provide: MatDialogRef, useValue: {} }, + { + provide: AdminLocalGovernmentService, + useValue: {}, + }, + { + provide: TagService, + useValue: mockTagService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TagDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.html b/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.html new file mode 100644 index 0000000000..ca9a9bb41b --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.html @@ -0,0 +1,64 @@ +
+

{{ isEdit ? 'Edit' : 'Create New' }} Tag

+
+
+
+ +
+
+ + Name + + +
+
+
+ + +
{{ item.name }}
+
+
+
+
+
+ Status * + + Active + Inactive + +
+
+
+ info Warning:   Tag already exists. Choose a different Tag name. +
+
+
+ +
+ + +
+ + +
+
+
diff --git a/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.scss b/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.scss new file mode 100644 index 0000000000..2415666c48 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.scss @@ -0,0 +1,52 @@ +@use '../../../../../styles/colors'; + + +mat-dialog-title { + margin-bottom: 24px; +} + +.warning-banner { + margin-top: 8px; + background-color: green; + padding: 16px; + + .display-none { + display: none !important; + } +} + +.warning { + padding: 16px; + font-size: 14px; + background-color: rgba(colors.$field-warning-bg-color, 0.5); + border-radius: 8px; + display: flex; + align-items: center; + color: colors.$dark-contrast-text; + margin-top: 18px; + + mat-icon { + color: colors.$dark-contrast-text; + margin-right: 16px; + } +} + +.dialog { + padding: 24px; + + form { + display: grid; + grid-template-columns: 1fr; + row-gap: 24px; + column-gap: 24px; + margin-bottom: 24px; + + .dialog-field { + margin-top: 20px; + } + + .mat-mdc-form-field { + width: 100%; + } + } +} \ No newline at end of file diff --git a/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.ts b/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.ts new file mode 100644 index 0000000000..b99a2f481b --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag-dialog/tag-dialog.component.ts @@ -0,0 +1,104 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { TagDto } from '../../../../services/tag/tag.dto'; +import { TagService } from '../../../../services/tag/tag.service'; +import { TagCategoryService } from '../../../../services/tag/tag-category/tag-category.service'; +import { TagCategoryDto } from 'src/app/services/tag/tag-category/tag-category.dto'; +import { Subject, takeUntil } from 'rxjs'; +import { FormControl } from '@angular/forms'; + +@Component({ + selector: 'app-tag-dialog', + templateUrl: './tag-dialog.component.html', + styleUrls: ['./tag-dialog.component.scss'], +}) +export class TagDialogComponent implements OnInit { + destroy = new Subject(); + uuid = ''; + name = ''; + category: TagCategoryDto | undefined = { + uuid: '', + name: '', + }; + isActive = 'true'; + categoryId = ''; + + isLoading = false; + isEdit = false; + showNameWarning = false; + nameControl = new FormControl(); + + categories: TagCategoryDto[] = []; + + constructor( + @Inject(MAT_DIALOG_DATA) public data: TagDto | undefined, + private dialogRef: MatDialogRef, + private tagService: TagService, + private tagCategoryService: TagCategoryService, + ) { + if (data) { + this.uuid = data.uuid; + this.name = data.name; + this.category = data.category ? data.category : undefined; + this.categoryId = data.category ? data.category.uuid : ''; + this.isActive = data.isActive.toString(); + } + this.isEdit = !!data; + } + + async ngOnInit(): Promise { + this.fetchCategories(); + + this.tagCategoryService.$categories + .pipe(takeUntil(this.destroy)) + .subscribe((result: { data: TagCategoryDto[]; total: number }) => { + this.categories = result.data; + }); + } + + async fetchCategories() { + this.tagCategoryService.fetch(0, 0); + } + + async onSubmit() { + this.isLoading = true; + const dto: TagDto = { + uuid: this.uuid, + name: this.name, + category: this.categoryId && this.categoryId !== '' ? { + uuid: this.categoryId, + name: '', + } : undefined, + isActive: this.isActive === 'true', + }; + + if (this.isEdit) { + try { + await this.tagService.update(this.uuid, dto); + } catch (e) { + this.showWarning(); + this.isLoading = false; + return; + } + } else { + try { + await this.tagService.create(dto); + } catch (e) { + this.showWarning(); + this.isLoading = false; + return; + } + } + this.isLoading = false; + this.dialogRef.close(true); + } + + onChange() { + this.showNameWarning = false; + } + + private showWarning() { + this.showNameWarning = true; + this.nameControl.setErrors({"invalid": true}); + } +} diff --git a/alcs-frontend/src/app/features/admin/tag/tag.component.html b/alcs-frontend/src/app/features/admin/tag/tag.component.html new file mode 100644 index 0000000000..ff35f9e491 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag.component.html @@ -0,0 +1,77 @@ +
+
+
+
+ + Enter tag + + + + {{option}} + + + +
+
+ +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Number{{ row.number }}Name{{ row.name }}Category{{ row.category ? row.category.name : '' }}Status +
+ check_circle_outline + Active +
+
+ not_interested + Inactive +
+
Actions + + +
+ + +
diff --git a/alcs-frontend/src/app/features/admin/tag/tag.component.scss b/alcs-frontend/src/app/features/admin/tag/tag.component.scss new file mode 100644 index 0000000000..1757aee5a5 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag.component.scss @@ -0,0 +1,56 @@ +@use '../../../../styles/colors'; + +.container { + .actions-bar { + margin-top: 30px; + display: flex; + justify-content: space-between; + } + + .search-control { + margin-right: 24px; + display: inline-block; + } + + table { + margin: 35px 2px 30px 2px; + box-shadow: 2px 2px 8px 2px #00000040; + } + + .mdc-data-table__table { + min-width: unset!important; + width: 99%; + } + + .delete-btn { + color: colors.$error-color; + width: 48px!important; + min-width: unset!important; + } + + .mat-icon { + margin-right: 1px; + } + + .edit-btn { + color: colors.$primary-color-dark; + width: 48px!important; + min-width: unset!important; + } + + .mat-column-name { + width: 35%; + } + + .mat-column-category { + width: 35%; + } + + .mat-column-status { + width: 30%; + } + + .mat-column-actions { + width: 20%; + } +} diff --git a/alcs-frontend/src/app/features/admin/tag/tag.component.spec.ts b/alcs-frontend/src/app/features/admin/tag/tag.component.spec.ts new file mode 100644 index 0000000000..bb65e9edd2 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag.component.spec.ts @@ -0,0 +1,55 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { PaginatedTagResponse, TagService } from '../../../services/tag/tag.service'; +import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; + +import { TagComponent } from './tag.component'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { BehaviorSubject } from 'rxjs'; + +describe('TagComponent', () => { + let component: TagComponent; + let fixture: ComponentFixture; + let mockTagService: DeepMocked; + let mockDialog: DeepMocked; + let mockConfirmationDialogService: DeepMocked; + + beforeEach(async () => { + mockTagService = createMock(); + mockDialog = createMock(); + mockConfirmationDialogService = createMock(); + mockTagService.$tags = new BehaviorSubject({ data: [], total: 0 }); + + await TestBed.configureTestingModule({ + declarations: [TagComponent], + providers: [ + { + provide: TagService, + useValue: mockTagService, + }, + { + provide: MatDialog, + useValue: mockDialog, + }, + { + provide: ConfirmationDialogService, + useValue: mockConfirmationDialogService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + imports: [HttpClientTestingModule, MatAutocompleteModule], + }).compileComponents(); + + fixture = TestBed.createComponent(TagComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/admin/tag/tag.component.ts b/alcs-frontend/src/app/features/admin/tag/tag.component.ts new file mode 100644 index 0000000000..29343d6af8 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/tag/tag.component.ts @@ -0,0 +1,111 @@ +import { Component, OnInit } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Subject, takeUntil } from 'rxjs'; +import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { TagDialogComponent } from './tag-dialog/tag-dialog.component'; +import { TagService } from '../../../services/tag/tag.service'; +import { TagDto } from '../../../services/tag/tag.dto'; +import { PageEvent } from '@angular/material/paginator'; +import { ToastService } from '../../../services/toast/toast.service'; + +@Component({ + selector: 'app-tag', + templateUrl: './tag.component.html', + styleUrls: ['./tag.component.scss'], +}) +export class TagComponent implements OnInit { + destroy = new Subject(); + pageIndex = 0; + itemsPerPage = 20; + search?: string = undefined; + tags: TagDto[] = []; + total: number = 0; + displayedColumns: string[] = ['name', 'category', 'isActive', 'actions']; + filteredOptions: string[] = []; + + constructor( + private tagService: TagService, + public dialog: MatDialog, + private confirmationDialogService: ConfirmationDialogService, + private toastService: ToastService + ) {} + + ngOnInit(): void { + this.fetch(); + + this.tagService.$tags + .pipe(takeUntil(this.destroy)) + .subscribe((result: { data: TagDto[]; total: number }) => { + this.tags = result.data; + this.filteredOptions = []; + this.total = result.total; + }); + } + + ngOnDestroy(): void { + this.destroy.next(); + this.destroy.complete(); + } + + onPageChange($event: PageEvent) { + this.pageIndex = $event.pageIndex; + this.itemsPerPage = $event.pageSize; + + this.fetch(); + } + + async fetch() { + this.tagService.fetch(this.pageIndex, this.itemsPerPage, this.search); + } + + async onCreate() { + const dialog = this.dialog.open(TagDialogComponent, { + minWidth: '600px', + maxWidth: '800px', + width: '70%', + }); + dialog.beforeClosed().subscribe(async (result) => { + if (result) { + await this.fetch(); + } + }); + } + + async onEdit(tagDto: TagDto) { + const dialog = this.dialog.open(TagDialogComponent, { + minWidth: '600px', + maxWidth: '800px', + width: '70%', + data: tagDto, + }); + dialog.beforeClosed().subscribe(async (result) => { + if (result) { + await this.fetch(); + } + }); + } + + async onDelete(tagDto: TagDto) { + this.confirmationDialogService + .openDialog({ + body: `Are you sure you want to delete ${tagDto.name}?`, + }) + .subscribe(async (answer) => { + if (answer) { + try { + await this.tagService.delete(tagDto.uuid); + } catch (e) { + this.toastService.showErrorToast('Tag is associated with files. Unable to delete.'); + } + await this.fetch(); + } + }); + } + + async updateFilter(value: string) { + const response = await this.tagService.search(0, 5, value); + if (response) { + this.filteredOptions = response.data.map((res) => res.name); + } + } +} diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts index 763c225ebd..308b692cf4 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts @@ -101,6 +101,13 @@ describe('ApplicationDetailsComponent', () => { soilHasSubmittedNotice: null, soilIsExtractionOrMining: null, soilIsFollowUp: null, + soilIsNewStructure: null, + soilStructureFarmUseReason: null, + soilStructureResidentialUseReason: null, + soilAgriParcelActivity: null, + soilStructureResidentialAccessoryUseReason: null, + soilStructureOtherUseReason: null, + soilProposedStructures: [], soilFollowUpIDs: null, soilProjectDuration: null, soilReduceNegativeImpacts: null, diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.html index f410cdd57f..cf865410d9 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.html @@ -127,9 +127,7 @@ -
- {{ i + 1 }} -
+
#{{ i + 1 }}
@@ -152,9 +150,7 @@ -
- {{ i + 1 }} -
+
#{{ i + 1 }}
{{ proposedResidence.floorArea }} m2
@@ -179,16 +175,15 @@
- -
Detailed Building Plan(s)
-
- +
Detailed Building Plan(s)
+
Describe the type and amount of fill proposed to be placed.
diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.html index e26a33bfdf..374bb61354 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.html @@ -1,4 +1,12 @@
+
Are you removing soil and placing fill in order to build a structure?
+
+ + {{ _applicationSubmission.soilIsNewStructure ? 'Yes' : 'No' }} + + +
+
Has the ALC previously received an application or Notice of Intent for this proposal?
@@ -120,13 +128,6 @@
-
- What alternative measures have you considered or attempted before proposing to place fill? -
-
- {{ _applicationSubmission.soilAlternativeMeasures }} -
-
Describe the type, origin and quality of fill proposed to be placed.
{{ _applicationSubmission.soilFillTypeToPlace }} @@ -136,10 +137,92 @@
{{ _applicationSubmission.soilTypeRemoved }}
-
What steps will be taken to reduce impacts to surrounding agricultural land?
-
- {{ _applicationSubmission.soilReduceNegativeImpacts }} -
+ + +
The total floor area (m2) for each of the proposed structure(s)
+
+
#
+
Type
+
Area
+ +
+ {{ i + 1 }} +
+
+ {{ mapStructureTypeValueToLabel(structure.type) }} + +
+
+ {{ structure.area }} m2 + +
+
+
+
+ +
+ + +
Describe how the structure is necessary for farm use
+
+ {{ _applicationSubmission.soilStructureFarmUseReason }} + +
+
+ + +
+ Describe why placing fill/removing soil is required for the residential structure(s) +
+
+ {{ _applicationSubmission.soilStructureResidentialUseReason }} + +
+
+ + +
Describe the current or proposed agricultural activity on the parcel(s)
+
+ {{ _applicationSubmission.soilAgriParcelActivity }} + +
+
+ + +
+ Describe the intended use of the residential accessory structure(s) and why placing fill/removing soil is + required +
+
+ {{ _applicationSubmission.soilStructureResidentialAccessoryUseReason }} + +
+
+ + +
+ Describe the intended use of the 'Other' structure(s) and why placing fill/removing soil is required +
+
+ {{ _applicationSubmission.soilStructureOtherUseReason }} + +
+
+
+ + +
+ What alternative measures have you considered or attempted before proposing to place fill? +
+
+ {{ _applicationSubmission.soilAlternativeMeasures }} +
+ +
What steps will be taken to reduce impacts to surrounding agricultural land?
+
+ {{ _applicationSubmission.soilReduceNegativeImpacts }} +
+
Proposal Map / Site Plan
@@ -147,41 +230,57 @@ {{ file.fileName }}
-
Cross Sections
-
- -
Reclamation Plan
- + -
Is your proposal for aggregate extraction or placer mining?
-
- - {{ _applicationSubmission.soilIsExtractionOrMining ? 'Yes' : 'No' }} - -
+ +
Detailed Building Plan(s)
+ +
-
- Have you submitted a Notice of Work to the Ministry of Energy, Mines and Low Carbon Innovation (EMLI)? -
-
- - {{ _applicationSubmission.soilHasSubmittedNotice ? 'Yes' : 'No' }} - -
+ +
Is your proposal for aggregate extraction or placer mining?
+
+ + {{ _applicationSubmission.soilIsExtractionOrMining ? 'Yes' : 'No' }} + +
-
Notice of Work
- +
+ Have you submitted a Notice of Work to the Ministry of Energy, Mines and Low Carbon Innovation (EMLI)? +
+
+ + {{ _applicationSubmission.soilHasSubmittedNotice ? 'Yes' : 'No' }} + +
+ +
Notice of Work
+ +
diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.scss b/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.scss index f4efa4f475..63d243debd 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.scss +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.scss @@ -9,3 +9,11 @@ height: 16px; } } + +.structure-table { + display: grid; + grid-template-columns: max-content max-content max-content; + overflow-x: auto; + grid-column-gap: 36px; + grid-row-gap: 12px; +} diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.ts index 88e9a6284e..f116acaa11 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.ts @@ -4,6 +4,8 @@ import { ApplicationDocumentDto } from '../../../../../services/application/appl import { ApplicationDocumentService } from '../../../../../services/application/application-document/application-document.service'; import { ApplicationSubmissionDto } from '../../../../../services/application/application.dto'; import { DOCUMENT_TYPE } from '../../../../../shared/document/document.dto'; +import { STRUCTURE_TYPES } from '../../../../../services/notice-of-intent/notice-of-intent.dto'; +import { STRUCTURE_TYPE_LABEL_MAP } from '../../../../notice-of-intent/applicant-info/notice-of-intent-details/additional-information/additional-information.component'; @Component({ selector: 'app-pfrs-details[applicationSubmission]', @@ -11,10 +13,35 @@ import { DOCUMENT_TYPE } from '../../../../../shared/document/document.dto'; styleUrls: ['./pfrs-details.component.scss'], }) export class PfrsDetailsComponent { + isSoilStructureFarmUseReasonVisible = false; + isSoilStructureResidentialUseReasonVisible = false; + isSoilAgriParcelActivityVisible = false; + isSoilStructureResidentialAccessoryUseReasonVisible = false; + isSoilOtherStructureVisible = false; + _applicationSubmission: ApplicationSubmissionDto | undefined; @Input() set applicationSubmission(application: ApplicationSubmissionDto | undefined) { if (application) { this._applicationSubmission = application; + + this.isSoilStructureFarmUseReasonVisible = application.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.FARM_STRUCTURE, + ); + this.isSoilStructureResidentialUseReasonVisible = application.soilProposedStructures.some( + (structure) => + structure.type === STRUCTURE_TYPES.PRINCIPAL_RESIDENCE || + structure.type === STRUCTURE_TYPES.ADDITIONAL_RESIDENCE || + structure.type === STRUCTURE_TYPES.ACCESSORY_STRUCTURE, + ); + this.isSoilAgriParcelActivityVisible = application.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.FARM_STRUCTURE, + ); + this.isSoilStructureResidentialAccessoryUseReasonVisible = application.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.ACCESSORY_STRUCTURE, + ); + this.isSoilOtherStructureVisible = application.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.OTHER, + ); } } @@ -23,6 +50,7 @@ export class PfrsDetailsComponent { this.crossSections = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.CROSS_SECTIONS); this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); this.reclamationPlans = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.RECLAMATION_PLAN); + this.buildingPlans = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.BUILDING_PLAN); this.noticeOfWork = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.NOTICE_OF_WORK); } } @@ -30,6 +58,7 @@ export class PfrsDetailsComponent { crossSections: ApplicationDocumentDto[] = []; proposalMap: ApplicationDocumentDto[] = []; reclamationPlans: ApplicationDocumentDto[] = []; + buildingPlans: ApplicationDocumentDto[] = []; noticeOfWork: ApplicationDocumentDto[] = []; constructor(private router: Router, private applicationDocumentService: ApplicationDocumentService) {} @@ -37,4 +66,12 @@ export class PfrsDetailsComponent { async openFile(file: ApplicationDocumentDto) { await this.applicationDocumentService.download(file.uuid, file.fileName); } + + mapStructureTypeValueToLabel(value: STRUCTURE_TYPES | null): string | null { + if (value === null) { + return null; + } + + return STRUCTURE_TYPE_LABEL_MAP[value]; + } } diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.html index 0d22a0273f..563e0dbb4a 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.html @@ -1,4 +1,12 @@
+
Are you removing soil and placing fill in order to build a structure?
+
+ + {{ _applicationSubmission.soilIsNewStructure ? 'Yes' : 'No' }} + + +
+
Has the ALC previously received an application or Notice of Intent for this proposal?
@@ -70,21 +78,96 @@ m
-
- What alternative measures have you considered or attempted before proposing to place fill? -
-
- {{ _applicationSubmission.soilAlternativeMeasures }} -
-
Describe the type, origin and quality of fill proposed to be placed.
{{ _applicationSubmission.soilFillTypeToPlace }}
-
What steps will be taken to reduce impacts to surrounding agricultural land?
-
- {{ _applicationSubmission.soilReduceNegativeImpacts }} -
+ + +
The total floor area (m2) for each of the proposed structure(s)
+
+
#
+
Type
+
Area
+ +
+ {{ i + 1 }} +
+
+ {{ mapStructureTypeValueToLabel(structure.type) }} + +
+
+ {{ structure.area }} m2 + +
+
+
+
+ +
+ + +
Describe how the structure is necessary for farm use
+
+ {{ _applicationSubmission.soilStructureFarmUseReason }} + +
+
+ + +
+ Describe why placing fill/removing soil is required for the residential structure(s) +
+
+ {{ _applicationSubmission.soilStructureResidentialUseReason }} + +
+
+ + +
Describe the current or proposed agricultural activity on the parcel(s)
+
+ {{ _applicationSubmission.soilAgriParcelActivity }} + +
+
+ + +
+ Describe the intended use of the residential accessory structure(s) and why placing fill/removing soil is + required +
+
+ {{ _applicationSubmission.soilStructureResidentialAccessoryUseReason }} + +
+
+ + +
+ Describe the intended use of the 'Other' structure(s) and why placing fill/removing soil is required +
+
+ {{ _applicationSubmission.soilStructureOtherUseReason }} + +
+
+
+ + +
+ What alternative measures have you considered or attempted before proposing to place fill? +
+
+ {{ _applicationSubmission.soilAlternativeMeasures }} +
+ +
What steps will be taken to reduce impacts to surrounding agricultural land?
+
+ {{ _applicationSubmission.soilReduceNegativeImpacts }} +
+
Proposal Map / Site Plan
@@ -92,16 +175,30 @@ {{ file.fileName }}
-
Cross Sections
- -
Reclamation Plan
- + + +
Cross Sections
+ + +
Reclamation Plan
+ +
+ + +
Detailed Building Plan(s)
+ +
diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.scss b/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.scss index e69de29bb2..8c01a936dc 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.scss +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.scss @@ -0,0 +1,7 @@ +.structure-table { + display: grid; + grid-template-columns: max-content max-content max-content; + overflow-x: auto; + grid-column-gap: 36px; + grid-row-gap: 12px; +} diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.ts index 2b4f326bbc..0fb6cd6ff8 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.ts @@ -4,6 +4,8 @@ import { ApplicationDocumentDto } from '../../../../../services/application/appl import { ApplicationDocumentService } from '../../../../../services/application/application-document/application-document.service'; import { ApplicationSubmissionDto } from '../../../../../services/application/application.dto'; import { DOCUMENT_TYPE } from '../../../../../shared/document/document.dto'; +import { STRUCTURE_TYPES } from '../../../../../services/notice-of-intent/notice-of-intent.dto'; +import { STRUCTURE_TYPE_LABEL_MAP } from '../../../../notice-of-intent/applicant-info/notice-of-intent-details/additional-information/additional-information.component'; @Component({ selector: 'app-pofo-details[applicationSubmission]', @@ -11,10 +13,35 @@ import { DOCUMENT_TYPE } from '../../../../../shared/document/document.dto'; styleUrls: ['./pofo-details.component.scss'], }) export class PofoDetailsComponent { + isSoilStructureFarmUseReasonVisible = false; + isSoilStructureResidentialUseReasonVisible = false; + isSoilAgriParcelActivityVisible = false; + isSoilStructureResidentialAccessoryUseReasonVisible = false; + isSoilOtherStructureVisible = false; + _applicationSubmission: ApplicationSubmissionDto | undefined; @Input() set applicationSubmission(application: ApplicationSubmissionDto | undefined) { if (application) { this._applicationSubmission = application; + + this.isSoilStructureFarmUseReasonVisible = application.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.FARM_STRUCTURE, + ); + this.isSoilStructureResidentialUseReasonVisible = application.soilProposedStructures.some( + (structure) => + structure.type === STRUCTURE_TYPES.PRINCIPAL_RESIDENCE || + structure.type === STRUCTURE_TYPES.ADDITIONAL_RESIDENCE || + structure.type === STRUCTURE_TYPES.ACCESSORY_STRUCTURE, + ); + this.isSoilAgriParcelActivityVisible = application.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.FARM_STRUCTURE, + ); + this.isSoilStructureResidentialAccessoryUseReasonVisible = application.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.ACCESSORY_STRUCTURE, + ); + this.isSoilOtherStructureVisible = application.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.OTHER, + ); } } @@ -23,16 +50,26 @@ export class PofoDetailsComponent { this.crossSections = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.CROSS_SECTIONS); this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); this.reclamationPlans = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.RECLAMATION_PLAN); + this.buildingPlans = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.BUILDING_PLAN); } } crossSections: ApplicationDocumentDto[] = []; proposalMap: ApplicationDocumentDto[] = []; reclamationPlans: ApplicationDocumentDto[] = []; + buildingPlans: ApplicationDocumentDto[] = []; constructor(private router: Router, private applicationDocumentService: ApplicationDocumentService) {} async openFile(file: ApplicationDocumentDto) { await this.applicationDocumentService.download(file.uuid, file.fileName); } + + mapStructureTypeValueToLabel(value: STRUCTURE_TYPES | null): string | null { + if (value === null) { + return null; + } + + return STRUCTURE_TYPE_LABEL_MAP[value]; + } } diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.html index 8ec3a2ec4c..20d9f7f457 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.html @@ -1,4 +1,12 @@
+
Are you removing soil and placing fill in order to build a structure?
+
+ + {{ _applicationSubmission.soilIsNewStructure ? 'Yes' : 'No' }} + + +
+
Has the ALC previously received an application or Notice of Intent for this proposal?
@@ -76,10 +84,85 @@
{{ _applicationSubmission.soilTypeRemoved }}
-
What steps will be taken to reduce impacts to surrounding agricultural land?
-
- {{ _applicationSubmission.soilReduceNegativeImpacts }} -
+ + +
The total floor area (m2) for each of the proposed structure(s)
+
+
#
+
Type
+
Area
+ +
+ {{ i + 1 }} +
+
+ {{ mapStructureTypeValueToLabel(structure.type) }} + +
+
+ {{ structure.area }} m2 + +
+
+
+
+ +
+ + +
Describe how the structure is necessary for farm use
+
+ {{ _applicationSubmission.soilStructureFarmUseReason }} + +
+
+ + +
+ Describe why placing fill/removing soil is required for the residential structure(s) +
+
+ {{ _applicationSubmission.soilStructureResidentialUseReason }} + +
+
+ + +
Describe the current or proposed agricultural activity on the parcel(s)
+
+ {{ _applicationSubmission.soilAgriParcelActivity }} + +
+
+ + +
+ Describe the intended use of the residential accessory structure(s) and why placing fill/removing soil is + required +
+
+ {{ _applicationSubmission.soilStructureResidentialAccessoryUseReason }} + +
+
+ + +
+ Describe the intended use of the 'Other' structure(s) and why placing fill/removing soil is required +
+
+ {{ _applicationSubmission.soilStructureOtherUseReason }} + +
+
+
+ + +
What steps will be taken to reduce impacts to surrounding agricultural land?
+
+ {{ _applicationSubmission.soilReduceNegativeImpacts }} +
+
Proposal Map / Site Plan
@@ -87,16 +170,30 @@ {{ file.fileName }}
-
Cross Sections
- -
Reclamation Plan
- + + +
Cross Sections
+ + +
Reclamation Plan
+ +
+ + +
Detailed Building Plan(s)
+ +
diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.scss b/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.scss index e69de29bb2..8c01a936dc 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.scss +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.scss @@ -0,0 +1,7 @@ +.structure-table { + display: grid; + grid-template-columns: max-content max-content max-content; + overflow-x: auto; + grid-column-gap: 36px; + grid-row-gap: 12px; +} diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.ts index bd32944e31..355f600350 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.ts @@ -4,6 +4,8 @@ import { ApplicationDocumentDto } from '../../../../../services/application/appl import { ApplicationDocumentService } from '../../../../../services/application/application-document/application-document.service'; import { ApplicationSubmissionDto } from '../../../../../services/application/application.dto'; import { DOCUMENT_TYPE } from '../../../../../shared/document/document.dto'; +import { STRUCTURE_TYPES } from '../../../../../services/notice-of-intent/notice-of-intent.dto'; +import { STRUCTURE_TYPE_LABEL_MAP } from '../../../../notice-of-intent/applicant-info/notice-of-intent-details/additional-information/additional-information.component'; @Component({ selector: 'app-roso-details[applicationSubmission]', @@ -11,10 +13,35 @@ import { DOCUMENT_TYPE } from '../../../../../shared/document/document.dto'; styleUrls: ['./roso-details.component.scss'], }) export class RosoDetailsComponent { + isSoilStructureFarmUseReasonVisible = false; + isSoilStructureResidentialUseReasonVisible = false; + isSoilAgriParcelActivityVisible = false; + isSoilStructureResidentialAccessoryUseReasonVisible = false; + isSoilOtherStructureVisible = false; + _applicationSubmission: ApplicationSubmissionDto | undefined; @Input() set applicationSubmission(application: ApplicationSubmissionDto | undefined) { if (application) { this._applicationSubmission = application; + + this.isSoilStructureFarmUseReasonVisible = application.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.FARM_STRUCTURE, + ); + this.isSoilStructureResidentialUseReasonVisible = application.soilProposedStructures.some( + (structure) => + structure.type === STRUCTURE_TYPES.PRINCIPAL_RESIDENCE || + structure.type === STRUCTURE_TYPES.ADDITIONAL_RESIDENCE || + structure.type === STRUCTURE_TYPES.ACCESSORY_STRUCTURE, + ); + this.isSoilAgriParcelActivityVisible = application.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.FARM_STRUCTURE, + ); + this.isSoilStructureResidentialAccessoryUseReasonVisible = application.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.ACCESSORY_STRUCTURE, + ); + this.isSoilOtherStructureVisible = application.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.OTHER, + ); } } @@ -23,16 +50,26 @@ export class RosoDetailsComponent { this.crossSections = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.CROSS_SECTIONS); this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); this.reclamationPlans = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.RECLAMATION_PLAN); + this.buildingPlans = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.BUILDING_PLAN); } } crossSections: ApplicationDocumentDto[] = []; proposalMap: ApplicationDocumentDto[] = []; reclamationPlans: ApplicationDocumentDto[] = []; + buildingPlans: ApplicationDocumentDto[] = []; constructor(private router: Router, private applicationDocumentService: ApplicationDocumentService) {} async openFile(file: ApplicationDocumentDto) { await this.applicationDocumentService.download(file.uuid, file.fileName); } + + mapStructureTypeValueToLabel(value: STRUCTURE_TYPES | null): string | null { + if (value === null) { + return null; + } + + return STRUCTURE_TYPE_LABEL_MAP[value]; + } } diff --git a/alcs-frontend/src/app/features/application/application.component.html b/alcs-frontend/src/app/features/application/application.component.html index c3ebc88276..b3bd0e1254 100644 --- a/alcs-frontend/src/app/features/application/application.component.html +++ b/alcs-frontend/src/app/features/application/application.component.html @@ -8,6 +8,7 @@ [submissionStatusService]="applicationStatusService" [applicationDetailService]="applicationDetailService" [applicationSubmissionService]="applicationSubmissionService" + [isTagSectionHidden]="false" days="Business Days" heading="Application" > diff --git a/alcs-frontend/src/app/features/application/application.component.ts b/alcs-frontend/src/app/features/application/application.component.ts index 8823637cec..b69a7b2ecd 100644 --- a/alcs-frontend/src/app/features/application/application.component.ts +++ b/alcs-frontend/src/app/features/application/application.component.ts @@ -28,6 +28,8 @@ import { PostDecisionComponent } from './post-decision/post-decision.component'; import { ProposalComponent } from './proposal/proposal.component'; import { ReviewComponent } from './review/review.component'; import { ApplicationSubmissionStatusService } from '../../services/application/application-submission-status/application-submission-status.service'; +import { ApplicationTagService } from '../../services/application/application-tag/application-tag.service'; +import { FileTagService } from '../../services/common/file-tag.service'; export const unsubmittedRoutes = [ { @@ -170,6 +172,7 @@ export const appChildRoutes = [ selector: 'app-application', templateUrl: './application.component.html', styleUrls: ['./application.component.scss'], + providers: [{ provide: FileTagService, useClass: ApplicationTagService }], }) export class ApplicationComponent implements OnInit, OnDestroy { destroy = new Subject(); diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/additional-information/additional-information.component.html b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/additional-information/additional-information.component.html index b4b70bd909..75d78377c9 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/additional-information/additional-information.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/additional-information/additional-information.component.html @@ -8,7 +8,7 @@ -
The total floor area (m2) of the proposed structure(s)
+
The total floor area (m2) for each of the proposed structure(s)
#
Type
@@ -18,7 +18,7 @@ {{ i + 1 }}
- {{ structure.type }} + {{ mapStructureTypeValueToLabel(structure.type) }}
@@ -39,7 +39,12 @@ -
Describe how the structure is necessary for residential use
+
+ Describe why placing fill + removing soil + placing fill/removing soil is required + for the residential structure(s) +
{{ _noiSubmission.soilStructureResidentialUseReason }} @@ -47,7 +52,7 @@ -
Describe the current agricultural activity on the parcel(s)
+
Describe the current or proposed agricultural activity on the parcel(s)
{{ _noiSubmission.soilAgriParcelActivity }} @@ -55,7 +60,12 @@ -
Describe the intended use of the residential accessory structure
+
+ Describe the intended use of the residential accessory structure(s) and why + placing fill + removing soil + placing fill/removing soil is required +
{{ _noiSubmission.soilStructureResidentialAccessoryUseReason }} @@ -63,7 +73,12 @@ -
Describe the intended use of the 'Other' structure
+
+ Describe the intended use of the 'Other' structure(s) and why + placing fill + removing soil + placing fill/removing soil is required +
{{ _noiSubmission.soilStructureOtherUseReason }} diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/additional-information/additional-information.component.ts b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/additional-information/additional-information.component.ts index 26affab7bd..e0687da116 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/additional-information/additional-information.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/additional-information/additional-information.component.ts @@ -9,6 +9,14 @@ import { } from '../../../../../services/notice-of-intent/notice-of-intent.dto'; import { DOCUMENT_TYPE } from '../../../../../shared/document/document.dto'; +export const STRUCTURE_TYPE_LABEL_MAP: Record = { + [STRUCTURE_TYPES.FARM_STRUCTURE]: STRUCTURE_TYPES.FARM_STRUCTURE, + [STRUCTURE_TYPES.PRINCIPAL_RESIDENCE]: 'Principal Residence', + [STRUCTURE_TYPES.ADDITIONAL_RESIDENCE]: 'Additional Residence', + [STRUCTURE_TYPES.ACCESSORY_STRUCTURE]: 'Residential Accessory Structure', + [STRUCTURE_TYPES.OTHER]: STRUCTURE_TYPES.OTHER, +}; + @Component({ selector: 'app-additional-information', templateUrl: './additional-information.component.html', @@ -82,4 +90,12 @@ export class AdditionalInformationComponent { break; } } + + mapStructureTypeValueToLabel(value: STRUCTURE_TYPES | null): string | null { + if (value === null) { + return null; + } + + return STRUCTURE_TYPE_LABEL_MAP[value]; + } } diff --git a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.html b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.html index 9b07329f27..030abe96d8 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.html @@ -7,9 +7,10 @@ days="Calendar Days" [showStatus]="true" [submissionStatusService]="noticeOfIntentStatusService" + [isTagSectionHidden]="false" >
-