From f8f24ae35b4316b1560f6fc984f95bcfddcb133f Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Tue, 24 Jun 2025 21:58:04 +0000 Subject: [PATCH 01/37] refactor for ag grid --- src/app/modules/data/data.component.html | 17 ++++++- src/app/modules/data/data.component.ts | 60 ++++++++++++++++++++---- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/src/app/modules/data/data.component.html b/src/app/modules/data/data.component.html index b8dad0b0..16836e2e 100644 --- a/src/app/modules/data/data.component.html +++ b/src/app/modules/data/data.component.html @@ -7,8 +7,21 @@ actionText: 'Create Dataset', }" > +
+ @if (datasets.length) { + + } +
-
+ diff --git a/src/app/modules/data/data.component.ts b/src/app/modules/data/data.component.ts index 5d4f3789..0586104e 100644 --- a/src/app/modules/data/data.component.ts +++ b/src/app/modules/data/data.component.ts @@ -10,22 +10,37 @@ import { from, skip } from 'rxjs' import type { Dataset } from '@seed/api/dataset' import { UserService } from '@seed/api/user' import { PageComponent } from '@seed/components' +import { ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' +import { AgGridAngular } from 'ag-grid-angular' +import { ConfigService } from '@seed/services' @Component({ selector: 'seed-data', templateUrl: './data.component.html', encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [CommonModule, MatButtonModule, MatIconModule, MatSortModule, MatTableModule, PageComponent], + // changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + AgGridAngular, + CommonModule, + MatButtonModule, + MatIconModule, + MatSortModule, + MatTableModule, + PageComponent, + ], }) export class DataComponent implements OnInit, AfterViewInit { + private _configService = inject(ConfigService) private _route = inject(ActivatedRoute) private _router = inject(Router) private _userService = inject(UserService) - readonly sort = viewChild.required(MatSort) - datasetsDataSource = new MatTableDataSource() + // datasetsDataSource = new MatTableDataSource() + datasets: Dataset[] datasetsColumns = ['name', 'importfiles', 'updated_at', 'last_modified_by', 'actions'] + gridApi: GridApi + gridTheme$ = this._configService.gridTheme$ + columnDefs: ColDef[] ngOnInit(): void { this._init() @@ -38,12 +53,40 @@ export class DataComponent implements OnInit, AfterViewInit { }) } - createDataset(): void { - console.log('create dataset') + setColumnDefs() { + this.columnDefs = [ + { field: 'id', hide: true }, + { field: 'name', headerName: 'Name' }, + { field: 'importfiles', headerName: 'Files', valueGetter: ({ data }: { data: Dataset }) => data.importfiles.length }, + { field: 'updated_at', headerName: 'Updated At', valueGetter: ({ data }: { data: Dataset }) => new Date(data.updated_at).toLocaleDateString() }, + { field: 'last_modified_by', headerName: 'Last Modified By' }, + { field: 'actions', headerName: 'Actions', cellRenderer: this.actionsRenderer }, + ] + } + + actionsRenderer({ data }: { data: Dataset}) { + return ` +
+ plus + clear + edit +
+ ` } ngAfterViewInit(): void { - this.datasetsDataSource.sort = this.sort() + return + // this.datasetsDataSource.sort = this.sort() + } + + onGridReady(agGrid: GridReadyEvent) { + this.gridApi = agGrid.api + this.gridApi.sizeColumnsToFit() + // this.gridApi.addEventListener('cellClicked', this.onCellClicked.bind(this) as (event: CellClickedEvent) => void) + } + + createDataset(): void { + console.log('create dataset') } trackByFn(_index: number, { id }: Dataset) { @@ -51,6 +94,7 @@ export class DataComponent implements OnInit, AfterViewInit { } private _init() { - this.datasetsDataSource.data = this._route.snapshot.data.datasets as Dataset[] + this.setColumnDefs() + this.datasets = this._route.snapshot.data.datasets as Dataset[] } } From 3fe9cfe33d73d7ca060abaf5f14bd614e0fd4b5d Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 25 Jun 2025 19:08:52 +0000 Subject: [PATCH 02/37] temp - register ag gird in main.ts --- src/main.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main.ts b/src/main.ts index 140797f3..14c1329d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,18 @@ import { bootstrapApplication } from '@angular/platform-browser' +import { ClientSideRowModelModule, ColumnAutoSizeModule, EventApiModule, ModuleRegistry, PaginationModule, ValidationModule } from 'ag-grid-community' import { AppComponent } from 'app/app.component' import { appConfig } from 'app/app.config' +// TEMP - should be overwritten when analyses pr is merged + +ModuleRegistry.registerModules([ + ClientSideRowModelModule, + ColumnAutoSizeModule, + EventApiModule, + PaginationModule, + ValidationModule, +]) + bootstrapApplication(AppComponent, appConfig).catch((err: unknown) => { console.error(err) }) From d1f8d1fc2033294b28d2c0db8dff11d9bf2426aa Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 25 Jun 2025 19:39:14 +0000 Subject: [PATCH 03/37] renove unused class --- .../components/column-profiles/column-profiles.component.ts | 2 +- .../detail/grid/building-files-grid.component.ts | 2 +- .../detail/grid/documents-grid.component.ts | 4 ++-- .../inventory-detail/detail/grid/paired-grid.component.ts | 2 +- .../detail/grid/scenarios-grid.component.ts | 2 +- src/app/modules/inventory-detail/meters/meters.component.ts | 4 ++-- src/app/modules/inventory-detail/notes/notes.component.ts | 4 ++-- .../sensors/data-loggers/data-loggers-grid.component.ts | 4 ++-- .../sensor-readings/sensor-readings-grid.component.ts | 4 ++-- .../sensors/sensors/sensors-grid.component.ts | 4 ++-- src/app/modules/inventory-detail/ubids/ubids.component.ts | 6 +++--- src/app/modules/inventory-list/groups/groups.component.ts | 4 ++-- .../matching-criteria/matching-criteria.component.ts | 2 +- 13 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/@seed/components/column-profiles/column-profiles.component.ts b/src/@seed/components/column-profiles/column-profiles.component.ts index e1b2aa3b..33b33e84 100644 --- a/src/@seed/components/column-profiles/column-profiles.component.ts +++ b/src/@seed/components/column-profiles/column-profiles.component.ts @@ -178,7 +178,7 @@ export class ColumnProfilesComponent implements OnDestroy, OnInit { // return ` // // push_pin // diff --git a/src/app/modules/inventory-detail/detail/grid/building-files-grid.component.ts b/src/app/modules/inventory-detail/detail/grid/building-files-grid.component.ts index 2c557659..488ecbd6 100644 --- a/src/app/modules/inventory-detail/detail/grid/building-files-grid.component.ts +++ b/src/app/modules/inventory-detail/detail/grid/building-files-grid.component.ts @@ -56,7 +56,7 @@ export class BuildingFilesGridComponent implements OnInit { actionRenderer = () => { return `
- cloud_download + cloud_download
` } diff --git a/src/app/modules/inventory-detail/detail/grid/documents-grid.component.ts b/src/app/modules/inventory-detail/detail/grid/documents-grid.component.ts index 4c334f35..3f5c8f38 100644 --- a/src/app/modules/inventory-detail/detail/grid/documents-grid.component.ts +++ b/src/app/modules/inventory-detail/detail/grid/documents-grid.component.ts @@ -75,8 +75,8 @@ export class DocumentsGridComponent implements OnChanges, OnDestroy { actionRenderer = () => { return `
- cloud_download - clear + cloud_download + clear
` } diff --git a/src/app/modules/inventory-detail/detail/grid/paired-grid.component.ts b/src/app/modules/inventory-detail/detail/grid/paired-grid.component.ts index 66737cd0..3dcc9402 100644 --- a/src/app/modules/inventory-detail/detail/grid/paired-grid.component.ts +++ b/src/app/modules/inventory-detail/detail/grid/paired-grid.component.ts @@ -103,7 +103,7 @@ export class PairedGridComponent implements OnChanges, OnDestroy { } unpairRenderer = () => { - return 'clear' + return 'clear' } get gridHeight() { diff --git a/src/app/modules/inventory-detail/detail/grid/scenarios-grid.component.ts b/src/app/modules/inventory-detail/detail/grid/scenarios-grid.component.ts index 16342344..09e0f614 100644 --- a/src/app/modules/inventory-detail/detail/grid/scenarios-grid.component.ts +++ b/src/app/modules/inventory-detail/detail/grid/scenarios-grid.component.ts @@ -78,7 +78,7 @@ export class ScenariosGridComponent implements OnChanges { } actionRenderer = () => { - return 'clear' + return 'clear' } onCellClicked(event: CellClickedEvent) { diff --git a/src/app/modules/inventory-detail/meters/meters.component.ts b/src/app/modules/inventory-detail/meters/meters.component.ts index 13df459d..1b33c8f0 100644 --- a/src/app/modules/inventory-detail/meters/meters.component.ts +++ b/src/app/modules/inventory-detail/meters/meters.component.ts @@ -179,8 +179,8 @@ export class MetersComponent implements OnDestroy, OnInit { actionRenderer = () => { return `
- clear - ${this.groupIds.length ? 'edit' : ''} + clear + ${this.groupIds.length ? 'edit' : ''}
` } diff --git a/src/app/modules/inventory-detail/notes/notes.component.ts b/src/app/modules/inventory-detail/notes/notes.component.ts index 94cc799f..5bbe462e 100644 --- a/src/app/modules/inventory-detail/notes/notes.component.ts +++ b/src/app/modules/inventory-detail/notes/notes.component.ts @@ -147,8 +147,8 @@ export class NotesComponent implements OnDestroy, OnInit { const canEdit = params.data.type === 'Manually Created' return `
- clear - ${canEdit ? 'edit' : ''} + clear + ${canEdit ? 'edit' : ''}
` } diff --git a/src/app/modules/inventory-detail/sensors/data-loggers/data-loggers-grid.component.ts b/src/app/modules/inventory-detail/sensors/data-loggers/data-loggers-grid.component.ts index 005db30d..ed590f7b 100644 --- a/src/app/modules/inventory-detail/sensors/data-loggers/data-loggers-grid.component.ts +++ b/src/app/modules/inventory-detail/sensors/data-loggers/data-loggers-grid.component.ts @@ -86,8 +86,8 @@ export class DataLoggersGridComponent implements OnChanges { add Readings - edit - clear + edit + clear
` } diff --git a/src/app/modules/inventory-detail/sensors/sensor-readings/sensor-readings-grid.component.ts b/src/app/modules/inventory-detail/sensors/sensor-readings/sensor-readings-grid.component.ts index 0b1b6b27..d2d50601 100644 --- a/src/app/modules/inventory-detail/sensors/sensor-readings/sensor-readings-grid.component.ts +++ b/src/app/modules/inventory-detail/sensors/sensor-readings/sensor-readings-grid.component.ts @@ -74,8 +74,8 @@ export class SensorReadingsGridComponent implements OnChanges { actionRenderer() { return `
- edit - clear + edit + clear
` } diff --git a/src/app/modules/inventory-detail/sensors/sensors/sensors-grid.component.ts b/src/app/modules/inventory-detail/sensors/sensors/sensors-grid.component.ts index 5e23ec83..20a78f18 100644 --- a/src/app/modules/inventory-detail/sensors/sensors/sensors-grid.component.ts +++ b/src/app/modules/inventory-detail/sensors/sensors/sensors-grid.component.ts @@ -84,8 +84,8 @@ export class SensorsGridComponent implements OnChanges { actionRenderer() { return `
- edit - clear + edit + clear
` } diff --git a/src/app/modules/inventory-detail/ubids/ubids.component.ts b/src/app/modules/inventory-detail/ubids/ubids.component.ts index 16558239..6f90e339 100644 --- a/src/app/modules/inventory-detail/ubids/ubids.component.ts +++ b/src/app/modules/inventory-detail/ubids/ubids.component.ts @@ -116,7 +116,7 @@ export class UbidsComponent implements OnDestroy, OnInit { return `
- check_circle + check_circle
` } @@ -124,8 +124,8 @@ export class UbidsComponent implements OnDestroy, OnInit { actionRenderer = () => { return `
- edit - clear + edit + clear
` } diff --git a/src/app/modules/inventory-list/groups/groups.component.ts b/src/app/modules/inventory-list/groups/groups.component.ts index 497c176e..65d77422 100644 --- a/src/app/modules/inventory-list/groups/groups.component.ts +++ b/src/app/modules/inventory-list/groups/groups.component.ts @@ -90,8 +90,8 @@ export class GroupsComponent implements OnDestroy, OnInit { actionRenderer = () => { return `
- edit - clear + edit + clear
` } diff --git a/src/app/modules/organizations/columns/matching-criteria/matching-criteria.component.ts b/src/app/modules/organizations/columns/matching-criteria/matching-criteria.component.ts index 594d3223..df227f9e 100644 --- a/src/app/modules/organizations/columns/matching-criteria/matching-criteria.component.ts +++ b/src/app/modules/organizations/columns/matching-criteria/matching-criteria.component.ts @@ -78,7 +78,7 @@ export class MatchingCriteriaComponent implements OnDestroy { actionRenderer = () => { return `
- clear + clear
` } From b968b649fcfea5083aa87a96ad8a73b954839652 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 25 Jun 2025 21:18:26 +0000 Subject: [PATCH 04/37] datasets grid --- src/@seed/api/dataset/dataset.service.ts | 87 +++++++++--- src/app/modules/data/data.component.html | 62 +-------- src/app/modules/data/data.component.ts | 125 ++++++++++++------ src/app/modules/data/data.routes.ts | 17 +-- .../data/modal/form-modal.component.html | 24 ++++ .../data/modal/form-modal.component.ts | 61 +++++++++ .../meters/meters.component.ts | 3 +- .../sensors/sensors.component.ts | 2 +- 8 files changed, 249 insertions(+), 132 deletions(-) create mode 100644 src/app/modules/data/modal/form-modal.component.html create mode 100644 src/app/modules/data/modal/form-modal.component.ts diff --git a/src/@seed/api/dataset/dataset.service.ts b/src/@seed/api/dataset/dataset.service.ts index 9e82cfee..347a20f2 100644 --- a/src/@seed/api/dataset/dataset.service.ts +++ b/src/@seed/api/dataset/dataset.service.ts @@ -2,43 +2,94 @@ import type { HttpErrorResponse } from '@angular/common/http' import { HttpClient } from '@angular/common/http' import { inject, Injectable } from '@angular/core' import type { Observable } from 'rxjs' -import { catchError, map, of, ReplaySubject } from 'rxjs' +import { catchError, combineLatest, map, of, ReplaySubject, switchMap, tap } from 'rxjs' import { UserService } from '../user' import type { CountDatasetsResponse, Dataset, ListDatasetsResponse } from './dataset.types' +import { ErrorService } from '@seed/services' +import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' @Injectable({ providedIn: 'root' }) export class DatasetService { private _httpClient = inject(HttpClient) private _userService = inject(UserService) - + private _errorService = inject(ErrorService) + private _snackBar = inject(SnackBarService) private _datasetCount = new ReplaySubject(1) + private _datasets = new ReplaySubject(1) datasetCount$ = this._datasetCount.asObservable() + datasets$ = this._datasets.asObservable() + orgId: number constructor() { // Refresh dataset count only when the organization ID changes - this._userService.currentOrganizationId$.subscribe((organizationId) => { - this.countDatasets(organizationId).subscribe() - }) + this._userService.currentOrganizationId$.pipe( + tap((orgId) => { + this.orgId = orgId + this.list(this.orgId) + this.countDatasets(this.orgId) + }), + ).subscribe() + } + + list(organizationId: number) { + const url = `/api/v3/datasets/?organization_id=${organizationId}` + this._httpClient.get(url).pipe( + map(({ datasets }) => datasets), + tap((datasets) => { this._datasets.next(datasets) }), + ).subscribe() + } + + create(orgId: number, name: string): Observable { + const url = `/api/v3/datasets/?organization_id=${orgId}` + return this._httpClient.post(url, { name }).pipe( + tap((response) => { console.log('temp', response) }), + tap(() => { + this.countDatasets(orgId) + this.list(orgId) + this._snackBar.success('Dataset created successfully') + }), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error creating dataset count') + }), + ) } - listDatasets(organizationId: number): Observable { - return this._httpClient - .get(`/api/v3/datasets/?organization_id=${organizationId}`) - .pipe(map(({ datasets }) => datasets)) + update(orgId: number, datasetId: number, name: string): Observable { + const url = `/api/v3/datasets/${datasetId}/?organization_id=${orgId}` + return this._httpClient.put(url, { dataset: name }).pipe( + tap((response) => { console.log('temp', response) }), + tap(() => { + this.countDatasets(orgId) + this.list(orgId) + this._snackBar.success('Dataset updated successfully') + }), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error updating dataset') + }), + ) } - countDatasets(organizationId: number): Observable { - return this._httpClient.get(`/api/v3/datasets/count/?organization_id=${organizationId}`).pipe( - map(({ datasets_count }) => { - // This assumes that the organizationId passed in is the selected organization - this._datasetCount.next(datasets_count) - return datasets_count + delete(orgId: number, datasetId: number) { + const url = `/api/v3/datasets/${datasetId}/?organization_id=${orgId}` + return this._httpClient.delete(url).pipe( + tap(() => { + this.countDatasets(orgId) + this.list(orgId) + this._snackBar.success('Dataset deleted successfully') }), catchError((error: HttpErrorResponse) => { - // TODO toast or alert? also, better fallback value - console.error('Error occurred while counting datasets:', error.error) - return of(-1) + return this._errorService.handleError(error, 'Error deleting dataset') }), ) } + + countDatasets(orgId: number) { + this._httpClient.get(`/api/v3/datasets/count/?organization_id=${orgId}`).pipe( + map(({ datasets_count }) => datasets_count), + tap((datasetsCount) => { this._datasetCount.next(datasetsCount)}), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error fetching dataset count') + }), + ).subscribe() + } } diff --git a/src/app/modules/data/data.component.html b/src/app/modules/data/data.component.html index 16836e2e..64b1fcf7 100644 --- a/src/app/modules/data/data.component.html +++ b/src/app/modules/data/data.component.html @@ -7,7 +7,7 @@ actionText: 'Create Dataset', }" > -
+
@if (datasets.length) { }
- - - diff --git a/src/app/modules/data/data.component.ts b/src/app/modules/data/data.component.ts index 0586104e..49bca591 100644 --- a/src/app/modules/data/data.component.ts +++ b/src/app/modules/data/data.component.ts @@ -1,18 +1,19 @@ import { CommonModule } from '@angular/common' -import type { AfterViewInit, OnInit } from '@angular/core' -import { ChangeDetectionStrategy, Component, inject, viewChild, ViewEncapsulation } from '@angular/core' +import type { OnInit } from '@angular/core' +import { Component, inject, viewChild, ViewEncapsulation } from '@angular/core' import { MatButtonModule } from '@angular/material/button' +import { MatDialog } from '@angular/material/dialog' import { MatIconModule } from '@angular/material/icon' -import { MatSort, MatSortModule } from '@angular/material/sort' -import { MatTableDataSource, MatTableModule } from '@angular/material/table' import { ActivatedRoute, Router } from '@angular/router' -import { from, skip } from 'rxjs' -import type { Dataset } from '@seed/api/dataset' -import { UserService } from '@seed/api/user' -import { PageComponent } from '@seed/components' -import { ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' import { AgGridAngular } from 'ag-grid-angular' +import type { CellClickedEvent, ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' +import { filter, switchMap, tap } from 'rxjs' +import { type Dataset, DatasetService } from '@seed/api/dataset' +import { UserService } from '@seed/api/user' +import { DeleteModalComponent, PageComponent } from '@seed/components' import { ConfigService } from '@seed/services' +import { naturalSort } from '@seed/utils' +import { FormModalComponent } from './modal/form-modal.component' @Component({ selector: 'seed-data', @@ -24,33 +25,45 @@ import { ConfigService } from '@seed/services' CommonModule, MatButtonModule, MatIconModule, - MatSortModule, - MatTableModule, PageComponent, ], }) -export class DataComponent implements OnInit, AfterViewInit { +export class DataComponent implements OnInit { private _configService = inject(ConfigService) + private _datasetService = inject(DatasetService) private _route = inject(ActivatedRoute) private _router = inject(Router) private _userService = inject(UserService) readonly sort = viewChild.required(MatSort) - // datasetsDataSource = new MatTableDataSource() + private _dialog = inject(MatDialog) + columnDefs: ColDef[] datasets: Dataset[] datasetsColumns = ['name', 'importfiles', 'updated_at', 'last_modified_by', 'actions'] + existingNames: string[] = [] gridApi: GridApi gridTheme$ = this._configService.gridTheme$ - columnDefs: ColDef[] + orgId: number ngOnInit(): void { - this._init() - // Rerun resolver and initializer on org change - this._userService.currentOrganizationId$.pipe(skip(1)).subscribe(() => { - from(this._router.navigate([this._router.url])).subscribe(() => { - this._init() - }) - }) + // this._userService.currentOrganizationId$.pipe(skip(1)).subscribe(() => { + // from(this._router.navigate([this._router.url])).subscribe(() => { + // this._init() + // }) + // }) + + this._userService.currentOrganizationId$.pipe( + tap((orgId) => { + this.orgId = orgId + this._datasetService.list(orgId) + }), + switchMap(() => this._datasetService.datasets$), + tap((datasets) => { + this.datasets = datasets.sort((a, b) => naturalSort(a.name, b.name)) + this.existingNames = datasets.map((ds) => ds.name) + this.setColumnDefs() + }), + ).subscribe() } setColumnDefs() { @@ -64,37 +77,71 @@ export class DataComponent implements OnInit, AfterViewInit { ] } - actionsRenderer({ data }: { data: Dataset}) { + actionsRenderer() { return ` -
- plus - clear - edit +
+ + add + Data Files + + edit + clear
` } - ngAfterViewInit(): void { - return - // this.datasetsDataSource.sort = this.sort() - } - onGridReady(agGrid: GridReadyEvent) { this.gridApi = agGrid.api this.gridApi.sizeColumnsToFit() - // this.gridApi.addEventListener('cellClicked', this.onCellClicked.bind(this) as (event: CellClickedEvent) => void) + this.gridApi.addEventListener('cellClicked', this.onCellClicked.bind(this) as (event: CellClickedEvent) => void) } - createDataset(): void { - console.log('create dataset') + onCellClicked(event: CellClickedEvent) { + console.log('cell clicked', event) + if (event.colDef.field !== 'actions') return + + const target = event.event.target as HTMLElement + const action = target.closest('[data-action]')?.getAttribute('data-action') + const { id } = event.data as { id: number } + const dataset = this.datasets.find((ds) => ds.id === id) + + if (action === 'addDataFiles') { + console.log('add data files', dataset) + } else if (action === 'rename') { + this.editDataset(dataset) + } else if (action === 'delete') { + this.deleteDataset(dataset) + } } - trackByFn(_index: number, { id }: Dataset) { - return id + editDataset(dataset: Dataset) { + const existingNames = this.existingNames.filter((n) => n !== dataset.name) + this._dialog.open(FormModalComponent, { + width: '40rem', + data: { orgId: this.orgId, dataset, existingNames }, + }) + } + + deleteDataset(dataset: Dataset) { + const dialogRef = this._dialog.open(DeleteModalComponent, { + width: '40rem', + data: { model: 'Dataset', instance: dataset.name }, + }) + + dialogRef.afterClosed().pipe( + filter(Boolean), + switchMap(() => this._datasetService.delete(this.orgId, dataset.id)), + ).subscribe() } - private _init() { - this.setColumnDefs() - this.datasets = this._route.snapshot.data.datasets as Dataset[] + createDataset = () => { + this._dialog.open(FormModalComponent, { + width: '40rem', + data: { orgId: this.orgId, dataset: null, existingNames: this.existingNames }, + }) + } + + trackByFn(_index: number, { id }: Dataset) { + return id } } diff --git a/src/app/modules/data/data.routes.ts b/src/app/modules/data/data.routes.ts index 33aa17dc..dd53b6f5 100644 --- a/src/app/modules/data/data.routes.ts +++ b/src/app/modules/data/data.routes.ts @@ -1,6 +1,6 @@ import { inject } from '@angular/core' import type { Routes } from '@angular/router' -import { switchMap, take } from 'rxjs' +import { switchMap, take, tap } from 'rxjs' import { DatasetService } from '@seed/api/dataset' import { UserService } from '@seed/api/user' import { DataComponent } from './data.component' @@ -14,13 +14,7 @@ export default [ resolve: { datasets: () => { const datasetService = inject(DatasetService) - const userService = inject(UserService) - return userService.currentOrganizationId$.pipe( - take(1), - switchMap((organizationId) => { - return datasetService.listDatasets(organizationId) - }), - ) + return datasetService.datasets$ }, }, }, @@ -33,11 +27,10 @@ export default [ const datasetService = inject(DatasetService) const userService = inject(UserService) return userService.currentOrganizationId$.pipe( + // TODO retrieve a single dataset instead take(1), - switchMap((organizationId) => { - // TODO retrieve a single dataset instead - return datasetService.listDatasets(organizationId) - }), + tap((orgId) => { datasetService.list(orgId) }), + switchMap(() => datasetService.datasets$), ) }, }, diff --git a/src/app/modules/data/modal/form-modal.component.html b/src/app/modules/data/modal/form-modal.component.html new file mode 100644 index 00000000..63a93e85 --- /dev/null +++ b/src/app/modules/data/modal/form-modal.component.html @@ -0,0 +1,24 @@ +
+ +
{{ create ? 'Create' : 'Edit' }} Dataset
+
+ + +
+ + Dataset Name + + @if (form.controls.name?.hasError('valueExists')) { + This name already exists. + } + +
+ +
+ + + + + + +
\ No newline at end of file diff --git a/src/app/modules/data/modal/form-modal.component.ts b/src/app/modules/data/modal/form-modal.component.ts new file mode 100644 index 00000000..875d7d1a --- /dev/null +++ b/src/app/modules/data/modal/form-modal.component.ts @@ -0,0 +1,61 @@ +import { CommonModule } from '@angular/common' +import type { OnInit } from '@angular/core' +import { Component, inject } from '@angular/core' +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms' +import { MatButtonModule } from '@angular/material/button' +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog' +import { MatDividerModule } from '@angular/material/divider' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatIconModule } from '@angular/material/icon' +import { MatInputModule } from '@angular/material/input' +import type { Dataset } from '@seed/api/dataset' +import { DatasetService } from '@seed/api/dataset' +import { SEEDValidators } from '@seed/validators' + +@Component({ + selector: 'seed-dataset-form-modal', + templateUrl: './form-modal.component.html', + imports: [ + CommonModule, + MatButtonModule, + MatDialogModule, + MatDividerModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + ReactiveFormsModule, + ], +}) +export class FormModalComponent implements OnInit { + private _dialogRef = inject(MatDialogRef) + private _datasetService = inject(DatasetService) + data = inject(MAT_DIALOG_DATA) as { orgId: number; dataset: Dataset; existingNames?: string[] } + form = new FormGroup({ + name: new FormControl('', [Validators.required, SEEDValidators.uniqueValue(this.data.existingNames)]), + }) + create = this.data.dataset ? false : true + + ngOnInit() { + if (this.data.dataset) { + this.form.patchValue({ name: this.data.dataset.name }) + } + } + + onSubmit() { + if (!this.form.valid) return + + if (this.create) { + this._datasetService.create(this.data.orgId, this.form.value.name).subscribe(() => { + this.dismiss() + }) + } else { + this._datasetService.update(this.data.orgId, this.data.dataset.id, this.form.value.name).subscribe(() => { + this.dismiss() + }) + } + } + + dismiss() { + this._dialogRef.close() + } +} diff --git a/src/app/modules/inventory-detail/meters/meters.component.ts b/src/app/modules/inventory-detail/meters/meters.component.ts index 1b33c8f0..e57b7c94 100644 --- a/src/app/modules/inventory-detail/meters/meters.component.ts +++ b/src/app/modules/inventory-detail/meters/meters.component.ts @@ -113,7 +113,6 @@ export class MetersComponent implements OnDestroy, OnInit { this._meterService.list(this.orgId, this.viewId) this._meterService.listReadings(this.orgId, this.viewId, this.interval, this.excludedIds) this._groupsService.listForInventory(this.orgId, [this.viewId]) - this._cycleService.get(this.orgId) this._meterService.meters$.pipe( tap((meters) => { @@ -141,7 +140,7 @@ export class MetersComponent implements OnDestroy, OnInit { tap((cycles) => { this.cycles = cycles }), ).subscribe() - this._datasetService.listDatasets(this.orgId).pipe( + this._datasetService.datasets$.pipe( tap((datasets) => { this.datasets = datasets }), ).subscribe() } diff --git a/src/app/modules/inventory-detail/sensors/sensors.component.ts b/src/app/modules/inventory-detail/sensors/sensors.component.ts index 5cfad288..f0c1d6c1 100644 --- a/src/app/modules/inventory-detail/sensors/sensors.component.ts +++ b/src/app/modules/inventory-detail/sensors/sensors.component.ts @@ -82,7 +82,7 @@ export class SensorsComponent implements OnDestroy, OnInit { tap(() => { this._cycleService.get(this.orgId) }), switchMap(() => this._cycleService.cycles$), tap((cycles) => { this.cycleId = cycles.length ? cycles[0].id : null }), - switchMap(() => this._datasetService.listDatasets(this.orgId)), + switchMap(() => this._datasetService.datasets$), tap((datasets) => { this.datasetId = datasets.length ? datasets[0].id.toString() : null }), ) } From 38e8698ee36e8ad975bd21b8180dd9906e39b10f Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 25 Jun 2025 21:20:39 +0000 Subject: [PATCH 05/37] rename --- src/app/app.routes.ts | 2 +- .../data.component.html => datasets/datasets.component.html} | 0 .../{data/data.component.ts => datasets/datasets.component.ts} | 0 .../{data/data.routes.ts => datasets/datasets.routes.ts} | 2 +- .../modules/{data => datasets}/modal/form-modal.component.html | 0 .../modules/{data => datasets}/modal/form-modal.component.ts | 0 6 files changed, 2 insertions(+), 2 deletions(-) rename src/app/modules/{data/data.component.html => datasets/datasets.component.html} (100%) rename src/app/modules/{data/data.component.ts => datasets/datasets.component.ts} (100%) rename src/app/modules/{data/data.routes.ts => datasets/datasets.routes.ts} (94%) rename src/app/modules/{data => datasets}/modal/form-modal.component.html (100%) rename src/app/modules/{data => datasets}/modal/form-modal.component.ts (100%) diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 52bfbd88..c8bb6569 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -73,7 +73,7 @@ export const appRoutes: Route[] = [ }, { path: 'data', - loadChildren: () => import('app/modules/data/data.routes'), + loadChildren: () => import('app/modules/datasets/datasets.routes'), }, { path: 'documentation', title: 'Documentation', component: DocumentationComponent }, { diff --git a/src/app/modules/data/data.component.html b/src/app/modules/datasets/datasets.component.html similarity index 100% rename from src/app/modules/data/data.component.html rename to src/app/modules/datasets/datasets.component.html diff --git a/src/app/modules/data/data.component.ts b/src/app/modules/datasets/datasets.component.ts similarity index 100% rename from src/app/modules/data/data.component.ts rename to src/app/modules/datasets/datasets.component.ts diff --git a/src/app/modules/data/data.routes.ts b/src/app/modules/datasets/datasets.routes.ts similarity index 94% rename from src/app/modules/data/data.routes.ts rename to src/app/modules/datasets/datasets.routes.ts index dd53b6f5..1234cfbc 100644 --- a/src/app/modules/data/data.routes.ts +++ b/src/app/modules/datasets/datasets.routes.ts @@ -3,7 +3,7 @@ import type { Routes } from '@angular/router' import { switchMap, take, tap } from 'rxjs' import { DatasetService } from '@seed/api/dataset' import { UserService } from '@seed/api/user' -import { DataComponent } from './data.component' +import { DataComponent } from './datasets.component' export default [ { diff --git a/src/app/modules/data/modal/form-modal.component.html b/src/app/modules/datasets/modal/form-modal.component.html similarity index 100% rename from src/app/modules/data/modal/form-modal.component.html rename to src/app/modules/datasets/modal/form-modal.component.html diff --git a/src/app/modules/data/modal/form-modal.component.ts b/src/app/modules/datasets/modal/form-modal.component.ts similarity index 100% rename from src/app/modules/data/modal/form-modal.component.ts rename to src/app/modules/datasets/modal/form-modal.component.ts From eed0813a32bd9dec51bfa65ed4e24f0750d75d40 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 25 Jun 2025 21:30:35 +0000 Subject: [PATCH 06/37] route rename to datasets --- src/app/app.routes.ts | 2 +- src/app/core/navigation/navigation.service.ts | 8 +++---- .../modules/datasets/datasets.component.ts | 21 +++++++++++++------ src/app/modules/datasets/datasets.routes.ts | 6 +++--- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index c8bb6569..bd6911db 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -72,7 +72,7 @@ export const appRoutes: Route[] = [ loadChildren: () => import('app/modules/inventory/inventory.routes'), }, { - path: 'data', + path: 'datasets', loadChildren: () => import('app/modules/datasets/datasets.routes'), }, { path: 'documentation', title: 'Documentation', component: DocumentationComponent }, diff --git a/src/app/core/navigation/navigation.service.ts b/src/app/core/navigation/navigation.service.ts index 32b6d0b2..5c73cdaf 100644 --- a/src/app/core/navigation/navigation.service.ts +++ b/src/app/core/navigation/navigation.service.ts @@ -108,11 +108,11 @@ export class NavigationService { children: this.inventoryChildrenProperties, }, { - id: 'data', - title: 'Data', + id: 'datasets', + title: 'Datasets', type: 'basic', icon: 'fa-solid:sitemap', - link: '/data', + link: '/datasets', }, { id: 'organizations', @@ -275,7 +275,7 @@ export class NavigationService { this._datasetService.datasetCount$.subscribe((count) => { // Use a timeout to avoid the race condition where mainNavigation hasn't been registered yet setTimeout(() => { - this.updateBadge('data', 'mainNavigation', count) + this.updateBadge('datasets', 'mainNavigation', count) }) }) this.getNavigation() diff --git a/src/app/modules/datasets/datasets.component.ts b/src/app/modules/datasets/datasets.component.ts index 49bca591..fe4aa02a 100644 --- a/src/app/modules/datasets/datasets.component.ts +++ b/src/app/modules/datasets/datasets.component.ts @@ -17,7 +17,7 @@ import { FormModalComponent } from './modal/form-modal.component' @Component({ selector: 'seed-data', - templateUrl: './data.component.html', + templateUrl: './datasets.component.html', encapsulation: ViewEncapsulation.None, // changeDetection: ChangeDetectionStrategy.OnPush, imports: [ @@ -28,13 +28,12 @@ import { FormModalComponent } from './modal/form-modal.component' PageComponent, ], }) -export class DataComponent implements OnInit { +export class DatasetsComponent implements OnInit { private _configService = inject(ConfigService) private _datasetService = inject(DatasetService) private _route = inject(ActivatedRoute) private _router = inject(Router) private _userService = inject(UserService) - readonly sort = viewChild.required(MatSort) private _dialog = inject(MatDialog) columnDefs: ColDef[] datasets: Dataset[] @@ -69,7 +68,7 @@ export class DataComponent implements OnInit { setColumnDefs() { this.columnDefs = [ { field: 'id', hide: true }, - { field: 'name', headerName: 'Name' }, + { field: 'name', headerName: 'Name', cellRenderer: this.nameRenderer }, { field: 'importfiles', headerName: 'Files', valueGetter: ({ data }: { data: Dataset }) => data.importfiles.length }, { field: 'updated_at', headerName: 'Updated At', valueGetter: ({ data }: { data: Dataset }) => new Date(data.updated_at).toLocaleDateString() }, { field: 'last_modified_by', headerName: 'Last Modified By' }, @@ -77,6 +76,15 @@ export class DataComponent implements OnInit { ] } + nameRenderer({ value }: { value: string }) { + return ` +
+ ${value} + open_in_new +
+ ` + } + actionsRenderer() { return `
@@ -97,8 +105,7 @@ export class DataComponent implements OnInit { } onCellClicked(event: CellClickedEvent) { - console.log('cell clicked', event) - if (event.colDef.field !== 'actions') return + if (!['actions', 'name'].includes(event.colDef.field)) return const target = event.event.target as HTMLElement const action = target.closest('[data-action]')?.getAttribute('data-action') @@ -111,6 +118,8 @@ export class DataComponent implements OnInit { this.editDataset(dataset) } else if (action === 'delete') { this.deleteDataset(dataset) + } else if(action === 'detail') { + void this._router.navigate([`/datasets/${id}`]) } } diff --git a/src/app/modules/datasets/datasets.routes.ts b/src/app/modules/datasets/datasets.routes.ts index 1234cfbc..c4e0b2fe 100644 --- a/src/app/modules/datasets/datasets.routes.ts +++ b/src/app/modules/datasets/datasets.routes.ts @@ -3,13 +3,13 @@ import type { Routes } from '@angular/router' import { switchMap, take, tap } from 'rxjs' import { DatasetService } from '@seed/api/dataset' import { UserService } from '@seed/api/user' -import { DataComponent } from './datasets.component' +import { DatasetsComponent } from './datasets.component' export default [ { path: '', title: 'Data', - component: DataComponent, + component: DatasetsComponent, runGuardsAndResolvers: 'always', resolve: { datasets: () => { @@ -21,7 +21,7 @@ export default [ { path: ':id', title: 'TODO', - component: DataComponent, + component: DatasetsComponent, resolve: { data: () => { const datasetService = inject(DatasetService) From 5c3410b1a933a2ffd67444269b7e41b6c426781c Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Thu, 26 Jun 2025 15:43:56 +0000 Subject: [PATCH 07/37] dataset grid, dummy actions --- src/@seed/api/dataset/dataset.service.ts | 26 ++- src/@seed/api/dataset/dataset.types.ts | 10 +- .../datasets/dataset/dataset.component.html | 23 +++ .../datasets/dataset/dataset.component.ts | 163 ++++++++++++++++++ src/app/modules/datasets/dataset/index.ts | 1 + .../modules/datasets/datasets.component.html | 2 +- .../modules/datasets/datasets.component.ts | 4 +- src/app/modules/datasets/datasets.routes.ts | 5 +- src/app/modules/datasets/index.ts | 2 + .../notes/notes.component.html | 2 +- 10 files changed, 228 insertions(+), 10 deletions(-) create mode 100644 src/app/modules/datasets/dataset/dataset.component.html create mode 100644 src/app/modules/datasets/dataset/dataset.component.ts create mode 100644 src/app/modules/datasets/dataset/index.ts create mode 100644 src/app/modules/datasets/index.ts diff --git a/src/@seed/api/dataset/dataset.service.ts b/src/@seed/api/dataset/dataset.service.ts index 347a20f2..7882fa76 100644 --- a/src/@seed/api/dataset/dataset.service.ts +++ b/src/@seed/api/dataset/dataset.service.ts @@ -2,9 +2,9 @@ import type { HttpErrorResponse } from '@angular/common/http' import { HttpClient } from '@angular/common/http' import { inject, Injectable } from '@angular/core' import type { Observable } from 'rxjs' -import { catchError, combineLatest, map, of, ReplaySubject, switchMap, tap } from 'rxjs' +import { catchError, map, ReplaySubject, tap } from 'rxjs' import { UserService } from '../user' -import type { CountDatasetsResponse, Dataset, ListDatasetsResponse } from './dataset.types' +import type { CountDatasetsResponse, Dataset, DatasetResponse, ListDatasetsResponse } from './dataset.types' import { ErrorService } from '@seed/services' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' @@ -39,6 +39,16 @@ export class DatasetService { ).subscribe() } + get(orgId: number, datasetId: number): Observable { + const url = `/api/v3/datasets/${datasetId}/?organization_id=${orgId}` + return this._httpClient.get(url).pipe( + map(({ dataset }) => dataset), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error fetching dataset') + }), + ) + } + create(orgId: number, name: string): Observable { const url = `/api/v3/datasets/?organization_id=${orgId}` return this._httpClient.post(url, { name }).pipe( @@ -86,10 +96,20 @@ export class DatasetService { countDatasets(orgId: number) { this._httpClient.get(`/api/v3/datasets/count/?organization_id=${orgId}`).pipe( map(({ datasets_count }) => datasets_count), - tap((datasetsCount) => { this._datasetCount.next(datasetsCount)}), + tap((datasetsCount) => { this._datasetCount.next(datasetsCount) }), catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error fetching dataset count') }), ).subscribe() } + + deleteFile(orgId: number, fileId: number) { + const url = `/api/v3/import_files/${fileId}/?organization_id=${orgId}` + return this._httpClient.delete(url).pipe( + tap(() => { this._snackBar.success('File deleted successfully') }), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error deleting file') + }), + ) + } } diff --git a/src/@seed/api/dataset/dataset.types.ts b/src/@seed/api/dataset/dataset.types.ts index 83c17f0e..3e5147d8 100644 --- a/src/@seed/api/dataset/dataset.types.ts +++ b/src/@seed/api/dataset/dataset.types.ts @@ -1,14 +1,17 @@ // Subset type -type ImportFile = { +export type ImportFile = { created: string; modified: string; deleted: boolean; import_record: number; cycle: number; + cycle_name?: string; // used in dataset.component ag-grid file: string; uploaded_filename: string; cached_first_row: string; id: number; + source_type: string; + num_rows: number; } // Subset type @@ -39,3 +42,8 @@ export type CountDatasetsResponse = { status: 'success'; datasets_count: number; } + +export type DatasetResponse = { + status: 'success'; + dataset: Dataset; +} diff --git a/src/app/modules/datasets/dataset/dataset.component.html b/src/app/modules/datasets/dataset/dataset.component.html new file mode 100644 index 00000000..ce30f3af --- /dev/null +++ b/src/app/modules/datasets/dataset/dataset.component.html @@ -0,0 +1,23 @@ + +
+ @if (importFiles.length) { + + } +
+
\ No newline at end of file diff --git a/src/app/modules/datasets/dataset/dataset.component.ts b/src/app/modules/datasets/dataset/dataset.component.ts new file mode 100644 index 00000000..b47bf2ca --- /dev/null +++ b/src/app/modules/datasets/dataset/dataset.component.ts @@ -0,0 +1,163 @@ +import { CommonModule } from '@angular/common' +import type { OnDestroy, OnInit } from '@angular/core' +import { Component, inject } from '@angular/core' +import { MatDialog } from '@angular/material/dialog' +import { ActivatedRoute } from '@angular/router' +import { Cycle } from '@seed/api/cycle' +import { CycleService } from '@seed/api/cycle/cycle.service' +import type { Dataset, ImportFile } from '@seed/api/dataset' +import { DatasetService } from '@seed/api/dataset' +import { UserService } from '@seed/api/user' +import { DeleteModalComponent, PageComponent } from '@seed/components' +import { ConfigService } from '@seed/services' +import { naturalSort } from '@seed/utils' +import { AgGridAngular } from 'ag-grid-angular' +import { CellClickedEvent, ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' +import type { Observable } from 'rxjs' +import { filter, of, Subject, switchMap, tap } from 'rxjs' + +@Component({ + selector: 'seed-dataset', + templateUrl: './dataset.component.html', + imports: [ + AgGridAngular, + CommonModule, + PageComponent, + ], +}) +export class DatasetComponent implements OnDestroy, OnInit { + private _configService = inject(ConfigService) + private _cycleService = inject(CycleService) + private _datasetService = inject(DatasetService) + private _dialog = inject(MatDialog) + private _route = inject(ActivatedRoute) + private _userService = inject(UserService) + private readonly _unsubscribeAll$ = new Subject() + columnDefs: ColDef[] = [] + cycles: Cycle[] = [] + cyclesMap: Record + dataset: Dataset + datasetId = this._route.snapshot.params?.id as number + datasetName$: Observable + importFiles: ImportFile[] = [] + gridApi: GridApi + gridTheme$ = this._configService.gridTheme$ + orgId: number + + ngOnInit(): void { + this._userService.currentOrganizationId$.pipe( + tap((orgId) => { this.orgId = orgId }), + switchMap(() => this.getCycles()), + switchMap(() => this.getDataset()), + ).subscribe() + } + + getCycles() { + return this._cycleService.cycles$.pipe( + tap((cycles) => { + this.cycles = cycles + this.cyclesMap = cycles.reduce((acc, c) => ({ ...acc, [c.id]: c.name }), {}) + console.log('cyclesMap', this.cyclesMap) + }), + ) + } + + getDataset() { + return this._datasetService.get(this.orgId, this.datasetId).pipe( + tap((dataset) => { + this.dataset = dataset + this.formatImportFiles(dataset) + this.datasetName$ = of(dataset.name) + this.setColumnDefs() + }), + ) + } + + formatImportFiles(dataset: Dataset) { + const { importfiles } = dataset + this.importFiles = importfiles.map((f) => ({ ...f, cycle_name: this.cyclesMap[f.cycle] })) + this.importFiles.sort((a, b) => naturalSort(b.created, a.created)) + } + + setColumnDefs() { + this.columnDefs = [ + { field: 'id', hide: true }, + { field: 'uploaded_filename', headerName: 'File Name' }, + { field: 'created', headerName: 'Date Imported', valueFormatter: ({ value }: { value: string }) => new Date(value).toLocaleDateString() }, + { field: 'source_type', headerName: 'Source Type' }, + { field: 'num_rows', headerName: 'Record Count' }, + { field: 'cycle_name', headerName: 'Cycle' }, + { field: 'actions', headerName: 'Actions', cellRenderer: this.actionsRenderer, width: 400, suppressSizeToFit: true }, + ] + } + + actionsRenderer() { + return ` +
+ + add + Data Mapping + + + add + Data Pairing + + cloud_download + clear +
+ ` + } + + onGridReady(agGrid: GridReadyEvent) { + this.gridApi = agGrid.api + this.gridApi.sizeColumnsToFit() + this.gridApi.addEventListener('cellClicked', this.onCellClicked.bind(this) as (event: CellClickedEvent) => void) + } + + onCellClicked(event: CellClickedEvent) { + if (event.colDef.field !== 'actions') return + + const target = event.event.target as HTMLElement + const action = target.closest('[data-action]')?.getAttribute('data-action') + const { id } = event.data as { id: number } + + const importFile = this.importFiles.find((f) => f.id === id) + + if (action === 'delete') { + this.deleteImportFile(importFile) + } else if (action === 'download') { + this.downloadDocument(importFile.file, importFile.uploaded_filename) + } else if (action === 'dataMapping') { + console.log('data mapping', importFile) + } else if (action === 'dataPairing') { + console.log('data pairing', importFile) + } + } + + deleteImportFile(importFile: ImportFile) { + const dialogRef = this._dialog.open(DeleteModalComponent, { + width: '40rem', + data: { model: 'Import File', instance: importFile.uploaded_filename }, + }) + + dialogRef.afterClosed().pipe( + filter(Boolean), + switchMap(() => this._datasetService.deleteFile(this.orgId, importFile.id)), + switchMap(() => this.getDataset()), + ).subscribe() + } + + downloadDocument(file: string, filename: string) { + console.log('file', file) + const a = document.createElement('a') + const url = file + a.href = url + a.download = filename + a.click() + } + + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } +} diff --git a/src/app/modules/datasets/dataset/index.ts b/src/app/modules/datasets/dataset/index.ts new file mode 100644 index 00000000..0a65d5ba --- /dev/null +++ b/src/app/modules/datasets/dataset/index.ts @@ -0,0 +1 @@ +export * from './dataset.component' diff --git a/src/app/modules/datasets/datasets.component.html b/src/app/modules/datasets/datasets.component.html index 64b1fcf7..4901d1d5 100644 --- a/src/app/modules/datasets/datasets.component.html +++ b/src/app/modules/datasets/datasets.component.html @@ -7,7 +7,7 @@ actionText: 'Create Dataset', }" > -
+
@if (datasets.length) { { const datasetService = inject(DatasetService) diff --git a/src/app/modules/datasets/index.ts b/src/app/modules/datasets/index.ts new file mode 100644 index 00000000..b949ec84 --- /dev/null +++ b/src/app/modules/datasets/index.ts @@ -0,0 +1,2 @@ +export * from './datasets.component' +export * from './dataset' diff --git a/src/app/modules/inventory-detail/notes/notes.component.html b/src/app/modules/inventory-detail/notes/notes.component.html index a8e1d28e..4e7deb38 100644 --- a/src/app/modules/inventory-detail/notes/notes.component.html +++ b/src/app/modules/inventory-detail/notes/notes.component.html @@ -10,7 +10,7 @@ action: createNote, }" > -
+
@if (rowData.length) { Date: Thu, 26 Jun 2025 16:20:07 +0000 Subject: [PATCH 08/37] icons --- src/app/modules/datasets/dataset/dataset.component.html | 2 +- src/app/modules/datasets/dataset/dataset.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/modules/datasets/dataset/dataset.component.html b/src/app/modules/datasets/dataset/dataset.component.html index ce30f3af..84a6b9f6 100644 --- a/src/app/modules/datasets/dataset/dataset.component.html +++ b/src/app/modules/datasets/dataset/dataset.component.html @@ -15,7 +15,7 @@ [domLayout]="'autoHeight'" [pagination]="true" [paginationPageSizeSelector]="[10, 20, 50, 100]" - [paginationPageSize]="10" + [paginationPageSize]="20" (gridReady)="onGridReady($event)" > } diff --git a/src/app/modules/datasets/dataset/dataset.component.ts b/src/app/modules/datasets/dataset/dataset.component.ts index b47bf2ca..da833dce 100644 --- a/src/app/modules/datasets/dataset/dataset.component.ts +++ b/src/app/modules/datasets/dataset/dataset.component.ts @@ -95,12 +95,12 @@ export class DatasetComponent implements OnDestroy, OnInit { return `
- add Data Mapping + open_in_new - add Data Pairing + open_in_new cloud_download clear From 3740ead55b2a7c8036430ebc70cfb487c73cb466 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Thu, 26 Jun 2025 22:00:36 +0000 Subject: [PATCH 09/37] basic upload to mapping --- src/@seed/api/dataset/dataset.service.ts | 13 +- src/@seed/api/dataset/dataset.types.ts | 5 + .../data-mappings/data-mapping.component.html | 49 ++++++ .../data-mappings/data-mapping.component.ts | 55 ++++++ .../modules/datasets/data-mappings/index.ts | 1 + .../data-upload-modal.component.html | 25 +++ .../data-upload-modal.component.ts | 38 ++++ .../property-taxlot-upload.component.html | 87 ++++++++++ .../property-taxlot-upload.component.ts | 164 ++++++++++++++++++ .../datasets/dataset/dataset.component.ts | 10 +- .../modules/datasets/datasets.component.ts | 14 +- src/app/modules/datasets/datasets.routes.ts | 6 + src/app/modules/datasets/index.ts | 1 + .../green-button-upload-modal.component.html | 2 +- 14 files changed, 461 insertions(+), 9 deletions(-) create mode 100644 src/app/modules/datasets/data-mappings/data-mapping.component.html create mode 100644 src/app/modules/datasets/data-mappings/data-mapping.component.ts create mode 100644 src/app/modules/datasets/data-mappings/index.ts create mode 100644 src/app/modules/datasets/data-upload/data-upload-modal.component.html create mode 100644 src/app/modules/datasets/data-upload/data-upload-modal.component.ts create mode 100644 src/app/modules/datasets/data-upload/property-taxlot-upload.component.html create mode 100644 src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts diff --git a/src/@seed/api/dataset/dataset.service.ts b/src/@seed/api/dataset/dataset.service.ts index 7882fa76..c6adb53b 100644 --- a/src/@seed/api/dataset/dataset.service.ts +++ b/src/@seed/api/dataset/dataset.service.ts @@ -4,7 +4,7 @@ import { inject, Injectable } from '@angular/core' import type { Observable } from 'rxjs' import { catchError, map, ReplaySubject, tap } from 'rxjs' import { UserService } from '../user' -import type { CountDatasetsResponse, Dataset, DatasetResponse, ListDatasetsResponse } from './dataset.types' +import type { CountDatasetsResponse, Dataset, DatasetResponse, ImportFile, ImportFileResponse, ListDatasetsResponse } from './dataset.types' import { ErrorService } from '@seed/services' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' @@ -112,4 +112,15 @@ export class DatasetService { }), ) } + + getImportFile(orgId: number, fieldId: number): Observable { + const url = `/api/v3/import_files/${fieldId}/?organization_id=${orgId}` + return this._httpClient.get(url).pipe( + tap((response) => { console.log('temp', response) }), + map(({ import_file }) => import_file), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error fetching import file') + }), + ) + } } diff --git a/src/@seed/api/dataset/dataset.types.ts b/src/@seed/api/dataset/dataset.types.ts index 3e5147d8..17003a6d 100644 --- a/src/@seed/api/dataset/dataset.types.ts +++ b/src/@seed/api/dataset/dataset.types.ts @@ -47,3 +47,8 @@ export type DatasetResponse = { status: 'success'; dataset: Dataset; } + +export type ImportFileResponse = { + status: 'success'; + import_file: ImportFile; +} diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.html b/src/app/modules/datasets/data-mappings/data-mapping.component.html new file mode 100644 index 00000000..cd49eca1 --- /dev/null +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.html @@ -0,0 +1,49 @@ + +
+ + + + + + + + +
+
+ + + + +
+

+ HelpEmail Templates +

+

Custom Emails

+
+ Custom emails can be sent to Building Owners using the templates defined below. The email will be sent to the SEED record's Owner + Email address and is currently not configurable. The email 'from' address is the same as the server email address which is also used + to email users their account information. +
+
+ The email supports brace templating to pull in data from the SEED property record. For example, the snippet below will replace the + latitude and longitude from the SEED record. Other fields can be added, but make sure to use the SEED field name not the display name. +
+
+ "Your building's latitude and longitude is {{ '{{' }}latitude{{ '}}' }}, {{ '{{' }}longitude{{ '}}' }}!" +
+
+
+ + + + MAPPING! + + \ No newline at end of file diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.ts b/src/app/modules/datasets/data-mappings/data-mapping.component.ts new file mode 100644 index 00000000..ffafeb9d --- /dev/null +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.ts @@ -0,0 +1,55 @@ +import { CommonModule } from '@angular/common' +import type { OnDestroy, OnInit } from '@angular/core' +import { Component, inject } from '@angular/core' +import { MatButtonModule } from '@angular/material/button' +import { MatIconModule } from '@angular/material/icon' +import { MatSidenavModule } from '@angular/material/sidenav' +import { ActivatedRoute } from '@angular/router' +import { DatasetService } from '@seed/api/dataset' +import { UserService } from '@seed/api/user' +import { PageComponent } from '@seed/components' +import { AgGridAngular } from 'ag-grid-angular' +import { Subject, switchMap, take, tap } from 'rxjs' + +@Component({ + selector: 'seed-data-mapping', + templateUrl: './data-mapping.component.html', + imports: [ + AgGridAngular, + CommonModule, + MatIconModule, + PageComponent, + MatSidenavModule, + MatButtonModule, + ], +}) +export class DataMappingComponent implements OnDestroy, OnInit { + private readonly _unsubscribeAll$ = new Subject() + private _datasetService = inject(DatasetService) + private _router = inject(ActivatedRoute) + private _userService = inject(UserService) + helpOpened = false + fileId = this._router.snapshot.params.id as number + orgId: number + + ngOnInit(): void { + console.log('Data Mapping Component Initialized') + this._userService.currentOrganizationId$ + .pipe( + take(1), + tap((orgId) => this.orgId = orgId), + switchMap(() => this._datasetService.getImportFile(this.orgId, this.fileId)), + tap((importFile) => { console.log('import file', importFile) }), + ) + .subscribe() + } + + toggleHelp = () => { + this.helpOpened = !this.helpOpened + } + + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } +} diff --git a/src/app/modules/datasets/data-mappings/index.ts b/src/app/modules/datasets/data-mappings/index.ts new file mode 100644 index 00000000..b86d0eb2 --- /dev/null +++ b/src/app/modules/datasets/data-mappings/index.ts @@ -0,0 +1 @@ +export * from './data-mapping.component' diff --git a/src/app/modules/datasets/data-upload/data-upload-modal.component.html b/src/app/modules/datasets/data-upload/data-upload-modal.component.html new file mode 100644 index 00000000..2bab5be2 --- /dev/null +++ b/src/app/modules/datasets/data-upload/data-upload-modal.component.html @@ -0,0 +1,25 @@ +
+
+
+ +
+ +
+
Upload Property / Tax Lot Data
+
+
+
+ +
+
+ + + \ No newline at end of file diff --git a/src/app/modules/datasets/data-upload/data-upload-modal.component.ts b/src/app/modules/datasets/data-upload/data-upload-modal.component.ts new file mode 100644 index 00000000..4e86ac3e --- /dev/null +++ b/src/app/modules/datasets/data-upload/data-upload-modal.component.ts @@ -0,0 +1,38 @@ +import { CommonModule } from '@angular/common' +import type { OnInit } from '@angular/core' +import { Component, inject } from '@angular/core' +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog' +import { MatDividerModule } from '@angular/material/divider' +import { MatIconModule } from '@angular/material/icon' +import type { Dataset } from '@seed/api/dataset' +import { PropertyTaxlotUploadComponent } from './property-taxlot-upload.component' +import { Cycle } from '@seed/api/cycle' + +@Component({ + selector: 'seed-data-upload-modal', + templateUrl: './data-upload-modal.component.html', + imports: [ + CommonModule, + MatDialogModule, + MatDividerModule, + MatIconModule, + PropertyTaxlotUploadComponent, + ], +}) +export class UploadFileModalComponent implements OnInit { + private _dialogRef = inject(MatDialogRef) + + data = inject(MAT_DIALOG_DATA) as { orgId: number; dataset: Dataset; cycles: Cycle[] } + + ngOnInit() { + return + } + + onSubmit() { + return + } + + dismiss() { + this._dialogRef.close() + } +} diff --git a/src/app/modules/datasets/data-upload/property-taxlot-upload.component.html b/src/app/modules/datasets/data-upload/property-taxlot-upload.component.html new file mode 100644 index 00000000..4d3c2806 --- /dev/null +++ b/src/app/modules/datasets/data-upload/property-taxlot-upload.component.html @@ -0,0 +1,87 @@ + + + + +
+
+ + +
+ + {{ form.get('multiCycle').value ? 'Default Cycle' : 'Cycle'}} + + + @for (cycle of cycles; track $index) { + {{ cycle.name }} + } + + + + + + Multi-Cycle + +
+ + + + + + +
+
+ + .csv, .xls, .xslx +
+
Note: only the first sheet of multi-sheet Excel files will be imported.
+ + +
+ + .geojson, .json +
+ +
+ + .xml +
+
+ + +
+
+ + @if (uploading) { + + } @else { + +
+ } +
+ + + + @if (inProgress) { + + } + +
\ No newline at end of file diff --git a/src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts b/src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts new file mode 100644 index 00000000..64cf80da --- /dev/null +++ b/src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts @@ -0,0 +1,164 @@ +import { CommonModule } from '@angular/common' +import { HttpErrorResponse } from '@angular/common/http' +import type { ElementRef, OnDestroy, OnInit } from '@angular/core' +import { Component, EventEmitter, inject, Input, Output, ViewChild } from '@angular/core' +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms' +import { MatButtonModule } from '@angular/material/button' +import { MatCheckboxChange, MatCheckboxModule } from '@angular/material/checkbox' +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog' +import { MatDividerModule } from '@angular/material/divider' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatIconModule } from '@angular/material/icon' +import { MatInputModule } from '@angular/material/input' +import { MatProgressBarModule } from '@angular/material/progress-bar' +import { MatSelectModule } from '@angular/material/select' +import { MatStepper, MatStepperModule } from '@angular/material/stepper' +import { Router } from '@angular/router' +import { Cycle } from '@seed/api/cycle' +import { CycleService } from '@seed/api/cycle/cycle.service' +import type { Dataset } from '@seed/api/dataset' +import { DatasetService } from '@seed/api/dataset' +import { ProgressResponse } from '@seed/api/progress' +import { ProgressBarComponent } from '@seed/components' +import { ErrorService } from '@seed/services' +import { ProgressBarObj, UploaderService } from '@seed/services/uploader' +import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' +import { catchError, Subject, switchMap, takeUntil, tap } from 'rxjs' + +@Component({ + selector: 'seed-property-taxlot-upload', + templateUrl: './property-taxlot-upload.component.html', + imports: [ + CommonModule, + MatButtonModule, + MatCheckboxModule, + MatDialogModule, + MatDividerModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatProgressBarModule, + MatSelectModule, + MatStepperModule, + ReactiveFormsModule, + ProgressBarComponent, + ], +}) +export class PropertyTaxlotUploadComponent implements OnDestroy, OnInit { + @ViewChild('stepper') stepper!: MatStepper + @ViewChild('fileInput') fileInput: ElementRef + @Input() cycles: Cycle[] + @Input() dataset: Dataset + @Input() orgId: number + @Output() dismissModal = new EventEmitter() + private _datasetService = inject(DatasetService) + private _cycleService = inject(CycleService) + private _uploaderService = inject(UploaderService) + private _errorService = inject(ErrorService) + private _router = inject(Router) + private _snackBar = inject(SnackBarService) + private readonly _unsubscribeAll$ = new Subject() + allowedTypes: string + completed = { 1: false, 2: false } + file: File + fileId: number + inProgress = false + uploading = false + progressBarObj: ProgressBarObj = { + message: [], + progress: 0, + total: 100, + complete: false, + statusMessage: '', + progressLastUpdated: null, + progressLastChecked: null, + } + sourceType: 'Assessed Raw' | 'GeoJSON' | 'BuildingSync Raw' + + form = new FormGroup({ + cycleId: new FormControl(null, Validators.required), + multiCycle: new FormControl(false), + }) + + ngOnInit() { + this.form.patchValue({ cycleId: this.cycles[0]?.id }) + } + + step1(fileList: FileList) { + this.file = fileList?.[0] + const cycleId = this.form.get('cycleId')?.value + const multiCycle = this.form.get('multiCycle')?.value + this.uploading = true + + return this._uploaderService + .fileUpload(this.orgId, this.file, this.sourceType, this.dataset.id.toString()) + .pipe( + takeUntil(this._unsubscribeAll$), + tap(({ import_file_id }) => { + this.fileId = import_file_id + this.completed[1] = true + }), + switchMap(() => this._uploaderService.saveRawData(this.orgId, cycleId, this.fileId, multiCycle)), + tap(({ progress_key }: { progress_key: string }) => { + this.uploading = false + this.stepper.next() + this.step2(progress_key) + }), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error uploading file') + }), + ) + .subscribe() + } + + triggerUpload(sourceType: 'Assessed Raw' | 'GeoJSON' | 'BuildingSync Raw') { + this.sourceType = sourceType + const allowedMap = { + 'Assessed Raw': '.csv,.xls,.xlsx', + GeoJSON: '.geojson,application/geo+json', + 'BuildingSync Raw': '.xml,application/xml,text/xml', + } + this.allowedTypes = allowedMap[sourceType] + + setTimeout(() => { + this.fileInput.nativeElement.click() + }) + } + + step2(progressKey: string) { + this.inProgress = true + + const failureFn = () => { + this._snackBar.alert('File Upload Failed') + } + + const successFn = () => { + this._snackBar.success('Successfully uploaded file') + console.log(this.progressBarObj) + this.dismissModal.emit() + + void this._router.navigate(['/datasets/mappings', this.fileId]) + } + + this._uploaderService + .checkProgressLoop({ + progressKey, + offset: 0, + multiplier: 1, + failureFn, + successFn, + progressBarObj: this.progressBarObj, + }) + .subscribe() + } + + onSubmit() { + console.log('onSubmit') + return + } + + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } +} diff --git a/src/app/modules/datasets/dataset/dataset.component.ts b/src/app/modules/datasets/dataset/dataset.component.ts index da833dce..5e4bfdea 100644 --- a/src/app/modules/datasets/dataset/dataset.component.ts +++ b/src/app/modules/datasets/dataset/dataset.component.ts @@ -3,7 +3,11 @@ import type { OnDestroy, OnInit } from '@angular/core' import { Component, inject } from '@angular/core' import { MatDialog } from '@angular/material/dialog' import { ActivatedRoute } from '@angular/router' -import { Cycle } from '@seed/api/cycle' +import { AgGridAngular } from 'ag-grid-angular' +import type { CellClickedEvent, ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' +import type { Observable } from 'rxjs' +import { filter, of, Subject, switchMap, tap } from 'rxjs' +import type { Cycle } from '@seed/api/cycle' import { CycleService } from '@seed/api/cycle/cycle.service' import type { Dataset, ImportFile } from '@seed/api/dataset' import { DatasetService } from '@seed/api/dataset' @@ -11,10 +15,6 @@ import { UserService } from '@seed/api/user' import { DeleteModalComponent, PageComponent } from '@seed/components' import { ConfigService } from '@seed/services' import { naturalSort } from '@seed/utils' -import { AgGridAngular } from 'ag-grid-angular' -import { CellClickedEvent, ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' -import type { Observable } from 'rxjs' -import { filter, of, Subject, switchMap, tap } from 'rxjs' @Component({ selector: 'seed-dataset', diff --git a/src/app/modules/datasets/datasets.component.ts b/src/app/modules/datasets/datasets.component.ts index 4f73dc1f..b56712c4 100644 --- a/src/app/modules/datasets/datasets.component.ts +++ b/src/app/modules/datasets/datasets.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common' import type { OnInit } from '@angular/core' -import { Component, inject, viewChild, ViewEncapsulation } from '@angular/core' +import { Component, inject, ViewEncapsulation } from '@angular/core' import { MatButtonModule } from '@angular/material/button' import { MatDialog } from '@angular/material/dialog' import { MatIconModule } from '@angular/material/icon' @@ -14,6 +14,9 @@ import { DeleteModalComponent, PageComponent } from '@seed/components' import { ConfigService } from '@seed/services' import { naturalSort } from '@seed/utils' import { FormModalComponent } from './modal/form-modal.component' +import { UploadFileModalComponent } from './data-upload/data-upload-modal.component' +import { CycleService } from '@seed/api/cycle/cycle.service' +import { Cycle } from '@seed/api/cycle' @Component({ selector: 'seed-data', @@ -30,12 +33,14 @@ import { FormModalComponent } from './modal/form-modal.component' }) export class DatasetsComponent implements OnInit { private _configService = inject(ConfigService) + private _cycleService = inject(CycleService) private _datasetService = inject(DatasetService) private _route = inject(ActivatedRoute) private _router = inject(Router) private _userService = inject(UserService) private _dialog = inject(MatDialog) columnDefs: ColDef[] + cycles: Cycle[] = [] datasets: Dataset[] datasetsColumns = ['name', 'importfiles', 'updated_at', 'last_modified_by', 'actions'] existingNames: string[] = [] @@ -56,6 +61,8 @@ export class DatasetsComponent implements OnInit { this.orgId = orgId this._datasetService.list(orgId) }), + switchMap(() => this._cycleService.cycles$), + tap((cycles) => { this.cycles = cycles }), switchMap(() => this._datasetService.datasets$), tap((datasets) => { this.datasets = datasets.sort((a, b) => naturalSort(a.name, b.name)) @@ -113,7 +120,10 @@ export class DatasetsComponent implements OnInit { const dataset = this.datasets.find((ds) => ds.id === id) if (action === 'addDataFiles') { - console.log('add data files', dataset) + this._dialog.open(UploadFileModalComponent, { + width: '40rem', + data: { orgId: this.orgId, dataset, cycles: this.cycles }, + }) } else if (action === 'rename') { this.editDataset(dataset) } else if (action === 'delete') { diff --git a/src/app/modules/datasets/datasets.routes.ts b/src/app/modules/datasets/datasets.routes.ts index 3c3fd32e..408068e7 100644 --- a/src/app/modules/datasets/datasets.routes.ts +++ b/src/app/modules/datasets/datasets.routes.ts @@ -3,6 +3,7 @@ import type { Routes } from '@angular/router' import { switchMap, take, tap } from 'rxjs' import { DatasetService } from '@seed/api/dataset' import { UserService } from '@seed/api/user' +import { DataMappingComponent } from './data-mappings' import { DatasetComponent } from './dataset/dataset.component' import { DatasetsComponent } from './datasets.component' @@ -36,4 +37,9 @@ export default [ }, }, }, + { + path: 'mappings/:id', + title: 'Data Mappings', + component: DataMappingComponent, + }, ] satisfies Routes diff --git a/src/app/modules/datasets/index.ts b/src/app/modules/datasets/index.ts index b949ec84..7a4e3855 100644 --- a/src/app/modules/datasets/index.ts +++ b/src/app/modules/datasets/index.ts @@ -1,2 +1,3 @@ export * from './datasets.component' export * from './dataset' +export * from './data-mappings' \ No newline at end of file diff --git a/src/app/modules/inventory-detail/meters/modal/green-button-upload-modal.component.html b/src/app/modules/inventory-detail/meters/modal/green-button-upload-modal.component.html index 49a2faad..c4c8858a 100644 --- a/src/app/modules/inventory-detail/meters/modal/green-button-upload-modal.component.html +++ b/src/app/modules/inventory-detail/meters/modal/green-button-upload-modal.component.html @@ -89,7 +89,7 @@ } From ac8e658753aea307aca7bc7f75a596cbe4123430 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Fri, 27 Jun 2025 16:24:23 +0000 Subject: [PATCH 10/37] ptl data upload modal fxnal --- src/app/app.routes.ts | 2 +- src/app/core/navigation/navigation.service.ts | 8 +-- .../property-taxlot-upload.component.html | 37 +++++++++----- .../property-taxlot-upload.component.ts | 49 +++++++++++++------ .../modules/datasets/datasets.component.ts | 2 +- 5 files changed, 66 insertions(+), 32 deletions(-) diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index bd6911db..c8bb6569 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -72,7 +72,7 @@ export const appRoutes: Route[] = [ loadChildren: () => import('app/modules/inventory/inventory.routes'), }, { - path: 'datasets', + path: 'data', loadChildren: () => import('app/modules/datasets/datasets.routes'), }, { path: 'documentation', title: 'Documentation', component: DocumentationComponent }, diff --git a/src/app/core/navigation/navigation.service.ts b/src/app/core/navigation/navigation.service.ts index 5c73cdaf..32b6d0b2 100644 --- a/src/app/core/navigation/navigation.service.ts +++ b/src/app/core/navigation/navigation.service.ts @@ -108,11 +108,11 @@ export class NavigationService { children: this.inventoryChildrenProperties, }, { - id: 'datasets', - title: 'Datasets', + id: 'data', + title: 'Data', type: 'basic', icon: 'fa-solid:sitemap', - link: '/datasets', + link: '/data', }, { id: 'organizations', @@ -275,7 +275,7 @@ export class NavigationService { this._datasetService.datasetCount$.subscribe((count) => { // Use a timeout to avoid the race condition where mainNavigation hasn't been registered yet setTimeout(() => { - this.updateBadge('datasets', 'mainNavigation', count) + this.updateBadge('data', 'mainNavigation', count) }) }) this.getNavigation() diff --git a/src/app/modules/datasets/data-upload/property-taxlot-upload.component.html b/src/app/modules/datasets/data-upload/property-taxlot-upload.component.html index 4d3c2806..b7630e0b 100644 --- a/src/app/modules/datasets/data-upload/property-taxlot-upload.component.html +++ b/src/app/modules/datasets/data-upload/property-taxlot-upload.component.html @@ -1,6 +1,6 @@ - - + +
@@ -74,14 +74,27 @@ } - - - @if (inProgress) { - - } - + + + @if (inProgress) { + + } + + + + + +
+ + +
+
+ \ No newline at end of file diff --git a/src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts b/src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts index 64cf80da..b6373cbf 100644 --- a/src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts +++ b/src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts @@ -1,6 +1,7 @@ +import { StepperSelectionEvent } from '@angular/cdk/stepper' import { CommonModule } from '@angular/common' import { HttpErrorResponse } from '@angular/common/http' -import type { ElementRef, OnDestroy, OnInit } from '@angular/core' +import type { AfterViewInit, ElementRef, OnDestroy, OnInit } from '@angular/core' import { Component, EventEmitter, inject, Input, Output, ViewChild } from '@angular/core' import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms' import { MatButtonModule } from '@angular/material/button' @@ -13,7 +14,7 @@ import { MatInputModule } from '@angular/material/input' import { MatProgressBarModule } from '@angular/material/progress-bar' import { MatSelectModule } from '@angular/material/select' import { MatStepper, MatStepperModule } from '@angular/material/stepper' -import { Router } from '@angular/router' +import { Router, RouterModule } from '@angular/router' import { Cycle } from '@seed/api/cycle' import { CycleService } from '@seed/api/cycle/cycle.service' import type { Dataset } from '@seed/api/dataset' @@ -40,11 +41,12 @@ import { catchError, Subject, switchMap, takeUntil, tap } from 'rxjs' MatProgressBarModule, MatSelectModule, MatStepperModule, - ReactiveFormsModule, ProgressBarComponent, + ReactiveFormsModule, + RouterModule, ], }) -export class PropertyTaxlotUploadComponent implements OnDestroy, OnInit { +export class PropertyTaxlotUploadComponent implements AfterViewInit, OnDestroy { @ViewChild('stepper') stepper!: MatStepper @ViewChild('fileInput') fileInput: ElementRef @Input() cycles: Cycle[] @@ -59,7 +61,7 @@ export class PropertyTaxlotUploadComponent implements OnDestroy, OnInit { private _snackBar = inject(SnackBarService) private readonly _unsubscribeAll$ = new Subject() allowedTypes: string - completed = { 1: false, 2: false } + completed = { 1: false, 2: false, 3: false } file: File fileId: number inProgress = false @@ -79,8 +81,8 @@ export class PropertyTaxlotUploadComponent implements OnDestroy, OnInit { cycleId: new FormControl(null, Validators.required), multiCycle: new FormControl(false), }) - - ngOnInit() { + + ngAfterViewInit() { this.form.patchValue({ cycleId: this.cycles[0]?.id }) } @@ -133,11 +135,11 @@ export class PropertyTaxlotUploadComponent implements OnDestroy, OnInit { } const successFn = () => { + this.completed[2] = true this._snackBar.success('Successfully uploaded file') - console.log(this.progressBarObj) - this.dismissModal.emit() - - void this._router.navigate(['/datasets/mappings', this.fileId]) + setTimeout(() => { + this.stepper.next() + }) } this._uploaderService @@ -152,9 +154,28 @@ export class PropertyTaxlotUploadComponent implements OnDestroy, OnInit { .subscribe() } - onSubmit() { - console.log('onSubmit') - return + goToMapping() { + this.dismissModal.emit() + void this._router.navigate(['/data/mappings', this.fileId]) + } + + goToStep1() { + this.completed[3] = true + this.stepper.selectedIndex = 0 + } + + onStepChange(event: StepperSelectionEvent) { + const index = event.selectedIndex + if (index === 0) this.resetStepper() + } + + resetStepper() { + this.completed = { 1: false, 2: false, 3: false } + this.file = null + this.fileId = null + this.fileInput.nativeElement.value = '' + this.inProgress = false + this.uploading = false } ngOnDestroy(): void { diff --git a/src/app/modules/datasets/datasets.component.ts b/src/app/modules/datasets/datasets.component.ts index b56712c4..9d1acba2 100644 --- a/src/app/modules/datasets/datasets.component.ts +++ b/src/app/modules/datasets/datasets.component.ts @@ -129,7 +129,7 @@ export class DatasetsComponent implements OnInit { } else if (action === 'delete') { this.deleteDataset(dataset) } else if (action === 'detail') { - void this._router.navigate([`/datasets/${id}`]) + void this._router.navigate([`/data/${id}`]) } } From afcb9e31e9fd4f9aec900cd6776e02e989b98871 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Fri, 27 Jun 2025 16:41:18 +0000 Subject: [PATCH 11/37] import data to mapping --- src/@seed/api/mapping/index.ts | 2 + src/@seed/api/mapping/mapping.service.ts | 43 ++++++++++++++ src/@seed/api/mapping/mapping.types.ts | 18 ++++++ .../data-mappings/data-mapping.component.ts | 42 ++++++++++++-- .../data-mappings/help.component.html | 57 +++++++++++++++++++ .../datasets/data-mappings/help.component.ts | 0 6 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 src/@seed/api/mapping/index.ts create mode 100644 src/@seed/api/mapping/mapping.service.ts create mode 100644 src/@seed/api/mapping/mapping.types.ts create mode 100644 src/app/modules/datasets/data-mappings/help.component.html create mode 100644 src/app/modules/datasets/data-mappings/help.component.ts diff --git a/src/@seed/api/mapping/index.ts b/src/@seed/api/mapping/index.ts new file mode 100644 index 00000000..8882aa58 --- /dev/null +++ b/src/@seed/api/mapping/index.ts @@ -0,0 +1,2 @@ +export * from './mapping.service' +export * from './mapping.types' diff --git a/src/@seed/api/mapping/mapping.service.ts b/src/@seed/api/mapping/mapping.service.ts new file mode 100644 index 00000000..5ad9657b --- /dev/null +++ b/src/@seed/api/mapping/mapping.service.ts @@ -0,0 +1,43 @@ +import { HttpClient, HttpErrorResponse } from '@angular/common/http' +import { inject, Injectable } from '@angular/core' +import { ErrorService } from '@seed/services' +import { UserService } from '../user' +import { catchError, type Observable } from 'rxjs' +import type { FirstFiveRowsResponse, MappingSuggestionsResponse, RawColumnNamesResponse } from './mapping.types' + +@Injectable({ providedIn: 'root' }) +export class MappingService { + private _httpClient = inject(HttpClient) + private _errorService = inject(ErrorService) + private _userService = inject(UserService) + + mappingSuggestions(orgId: number, importFileId: number): Observable { + const url = `/api/v3/import_files/${importFileId}/mapping_suggestions/?organization_id=${orgId}` + return this._httpClient.get(url) + .pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error fetching mapping suggestions') + }), + ) + } + + rawColumnNames(orgId: number, importFileId: number): Observable { + const url = `/api/v3/import_files/${importFileId}/raw_column_names/?organization_id=${orgId}` + return this._httpClient.get(url) + .pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error fetching raw column names') + }), + ) + } + + firstFiveRows(orgId: number, importFileId: number): Observable { + const url = `/api/v3/import_files/${importFileId}/first_five_rows/?organization_id=${orgId}` + return this._httpClient.get(url) + .pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error fetching first five rows') + }), + ) + } +} diff --git a/src/@seed/api/mapping/mapping.types.ts b/src/@seed/api/mapping/mapping.types.ts new file mode 100644 index 00000000..45b8fad3 --- /dev/null +++ b/src/@seed/api/mapping/mapping.types.ts @@ -0,0 +1,18 @@ +import type { Column } from '../column' + +export type MappingSuggestionsResponse = { + status: string; + property_columns: Column[]; + suggested_column_mappings: Record; + taxlot_columns: Column[]; +} + +export type RawColumnNamesResponse = { + status: string; + raw_columns: string[]; +} + +export type FirstFiveRowsResponse = { + status: string; + first_five_rows: Record[]; +} diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.ts b/src/app/modules/datasets/data-mappings/data-mapping.component.ts index ffafeb9d..3f467a34 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.ts +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.ts @@ -5,11 +5,12 @@ import { MatButtonModule } from '@angular/material/button' import { MatIconModule } from '@angular/material/icon' import { MatSidenavModule } from '@angular/material/sidenav' import { ActivatedRoute } from '@angular/router' -import { DatasetService } from '@seed/api/dataset' +import { DatasetService, ImportFile } from '@seed/api/dataset' +import { FirstFiveRowsResponse, MappingService, MappingSuggestionsResponse, RawColumnNamesResponse } from '@seed/api/mapping' import { UserService } from '@seed/api/user' import { PageComponent } from '@seed/components' import { AgGridAngular } from 'ag-grid-angular' -import { Subject, switchMap, take, tap } from 'rxjs' +import { catchError, filter, forkJoin, of, Subject, switchMap, take, tap } from 'rxjs' @Component({ selector: 'seed-data-mapping', @@ -26,11 +27,16 @@ import { Subject, switchMap, take, tap } from 'rxjs' export class DataMappingComponent implements OnDestroy, OnInit { private readonly _unsubscribeAll$ = new Subject() private _datasetService = inject(DatasetService) + private _mappingService = inject(MappingService) private _router = inject(ActivatedRoute) private _userService = inject(UserService) helpOpened = false fileId = this._router.snapshot.params.id as number + importFile: ImportFile orgId: number + firstFiveRows: FirstFiveRowsResponse + mappingSuggestions: MappingSuggestionsResponse + rawColumnNames: RawColumnNamesResponse ngOnInit(): void { console.log('Data Mapping Component Initialized') @@ -38,12 +44,40 @@ export class DataMappingComponent implements OnDestroy, OnInit { .pipe( take(1), tap((orgId) => this.orgId = orgId), - switchMap(() => this._datasetService.getImportFile(this.orgId, this.fileId)), - tap((importFile) => { console.log('import file', importFile) }), + switchMap(() => this.getImportFile()), + filter(Boolean), + switchMap(() => this.getMappingData()), ) .subscribe() } + getImportFile() { + return this._datasetService.getImportFile(this.orgId, this.fileId) + .pipe( + take(1), + tap((importFile) => { this.importFile = importFile }), + catchError(() => { + console.log('bad importfile') + return of(null) + }), + ) + } + getMappingData() { + return forkJoin([ + this._mappingService.firstFiveRows(this.orgId, this.fileId), + this._mappingService.mappingSuggestions(this.orgId, this.fileId), + this._mappingService.rawColumnNames(this.orgId, this.fileId), + ]) + .pipe( + take(1), + tap(([firstFiveRows, mappingSuggestions, rawColumnNames]) => { + this.firstFiveRows = firstFiveRows + this.mappingSuggestions = mappingSuggestions + this.rawColumnNames = rawColumnNames + }), + ) + } + toggleHelp = () => { this.helpOpened = !this.helpOpened } diff --git a/src/app/modules/datasets/data-mappings/help.component.html b/src/app/modules/datasets/data-mappings/help.component.html new file mode 100644 index 00000000..a0868911 --- /dev/null +++ b/src/app/modules/datasets/data-mappings/help.component.html @@ -0,0 +1,57 @@ +
+
+ MAPPING YOUR DATA TO SEED +
+ +
+ It is necessary to map your field names to SEED field names. You can select from the list that appears as you start to + type, which is based on the Building Energy Data Exchange Specification (BEDES), or you can type in your own name, as + well as typing in the field name from the original datafile. +
+ +
+ In addition, you need to specify where the field should be associated with Tax Lot data or Property data. This will + affect how the data is matched and merged, as well as how it is displayed in the Inventory view. +
+ +
+ Column Mapping Profiles can be used to help you easily and consistently map your data. Note that file header columns + defined in the profile must match exactly (spaces, lowercase, uppercase, etc.) in order for the corresponding SEED + column information to be used. +
+ +
+ Field names for matching Properties: Custom ID 1, PM Property ID + +
+ +
+ Field names for matching Tax Lots: Custom ID 1, Jurisdiction Tax Lot ID + +
+ +
+ If there are fields in the datafile mapped to these names, the program will attempt to match on the corresponding values + in existing records. All of these fields must have the same values between records for the records to match. + +
+ +
+ Matches within the same cycle will be merged together, while matches in different cycles will be associated for + cross-cycle analysis. +
+ +
+ When you click the Map Your Data button, the program will show a grid with the new field names as the column headings + and your data in the rows. In that view, you can still come back to the initial mapping screen and change the field + mapping. +
+ +
+ + + + + + + diff --git a/src/app/modules/datasets/data-mappings/help.component.ts b/src/app/modules/datasets/data-mappings/help.component.ts new file mode 100644 index 00000000..e69de29b From c7a99b4d6772d4b76a4deb72188db34366561e7b Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Fri, 27 Jun 2025 17:21:26 +0000 Subject: [PATCH 12/37] help component --- .../data-mappings/help.component.html | 20 +++++++++---------- .../datasets/data-mappings/help.component.ts | 9 +++++++++ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/app/modules/datasets/data-mappings/help.component.html b/src/app/modules/datasets/data-mappings/help.component.html index a0868911..4727e294 100644 --- a/src/app/modules/datasets/data-mappings/help.component.html +++ b/src/app/modules/datasets/data-mappings/help.component.html @@ -1,47 +1,45 @@ -
+
MAPPING YOUR DATA TO SEED
-
+
It is necessary to map your field names to SEED field names. You can select from the list that appears as you start to type, which is based on the Building Energy Data Exchange Specification (BEDES), or you can type in your own name, as well as typing in the field name from the original datafile.
-
+
In addition, you need to specify where the field should be associated with Tax Lot data or Property data. This will affect how the data is matched and merged, as well as how it is displayed in the Inventory view.
-
+
Column Mapping Profiles can be used to help you easily and consistently map your data. Note that file header columns defined in the profile must match exactly (spaces, lowercase, uppercase, etc.) in order for the corresponding SEED column information to be used.
-
+
Field names for matching Properties: Custom ID 1, PM Property ID
-
+
Field names for matching Tax Lots: Custom ID 1, Jurisdiction Tax Lot ID -
-
+
If there are fields in the datafile mapped to these names, the program will attempt to match on the corresponding values in existing records. All of these fields must have the same values between records for the records to match. -
-
+
Matches within the same cycle will be merged together, while matches in different cycles will be associated for cross-cycle analysis.
-
+
When you click the Map Your Data button, the program will show a grid with the new field names as the column headings and your data in the rows. In that view, you can still come back to the initial mapping screen and change the field mapping. diff --git a/src/app/modules/datasets/data-mappings/help.component.ts b/src/app/modules/datasets/data-mappings/help.component.ts index e69de29b..a6c633e7 100644 --- a/src/app/modules/datasets/data-mappings/help.component.ts +++ b/src/app/modules/datasets/data-mappings/help.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core' + +@Component({ + selector: 'seed-data-mapping-help', + templateUrl: './help.component.html', + imports: [], +}) +export class HelpComponent { +} From e0fc3fc7cab5938d964f62cb50acdcd8acbfc31e Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Fri, 27 Jun 2025 21:59:06 +0000 Subject: [PATCH 13/37] mapping table dev --- src/@seed/api/cycle/cycle.service.ts | 31 +++- src/@seed/api/dataset/dataset.service.ts | 1 - src/@seed/api/mapping/mapping.service.ts | 8 +- src/@seed/api/mapping/mapping.types.ts | 4 +- .../data-mappings/data-mapping.component.html | 56 +++--- .../data-mappings/data-mapping.component.ts | 164 ++++++++++++++++-- .../datasets/dataset/dataset.component.ts | 4 +- .../sensors/sensors.component.ts | 3 - .../list/inventory.component.ts | 2 +- src/main.ts | 6 +- 10 files changed, 227 insertions(+), 52 deletions(-) diff --git a/src/@seed/api/cycle/cycle.service.ts b/src/@seed/api/cycle/cycle.service.ts index 6c6b02e1..31544f91 100644 --- a/src/@seed/api/cycle/cycle.service.ts +++ b/src/@seed/api/cycle/cycle.service.ts @@ -7,11 +7,12 @@ import { OrganizationService } from '@seed/api/organization' import { ErrorService } from '@seed/services' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' import type { Cycle, CycleResponse, CyclesResponse } from './cycle.types' +import { UserService } from '../user' @Injectable({ providedIn: 'root' }) export class CycleService { private _httpClient = inject(HttpClient) - private _organizationService = inject(OrganizationService) + private _userService = inject(UserService) private _snackBar = inject(SnackBarService) private _errorService = inject(ErrorService) private _cycles = new BehaviorSubject([]) @@ -20,21 +21,20 @@ export class CycleService { cycles$ = this._cycles.asObservable() constructor() { - this._organizationService.currentOrganization$ + this._userService.currentOrganizationId$ .pipe( - tap(({ org_id }) => { - this.get(org_id) + tap((orgId) => { + this.getCycles(orgId) }), ) .subscribe() } - get(orgId: number) { + getCycles(orgId: number) { const url = `/api/v3/cycles/?organization_id=${orgId}` this._httpClient .get(url) .pipe( - take(1), map(({ cycles }) => cycles), tap((cycles) => { this._cycles.next(cycles) @@ -46,12 +46,25 @@ export class CycleService { .subscribe() } + getCycle(orgId: number, cycleId: number): Observable { + const url = `/api/v3/cycles/${cycleId}?organization_id=${orgId}` + return this._httpClient + .get(url) + .pipe( + take(1), + map(({ cycles }) => cycles), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error fetching cycles') + }), + ) + } + post({ data, orgId }): Observable { const url = `/api/v3/cycles/?organization_id=${orgId}` return this._httpClient.post(url, data).pipe( tap((response) => { this._snackBar.success(`Created Cycle ${response.cycles.name}`) - this.get(orgId as number) + this.getCycles(orgId as number) }), catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error creating cycle') @@ -64,7 +77,7 @@ export class CycleService { return this._httpClient.put(url, data).pipe( tap((response) => { this._snackBar.success(`Updated Cycle ${response.cycles.name}`) - this.get(orgId as number) + this.getCycles(orgId as number) }), catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error updating cycle') @@ -76,7 +89,7 @@ export class CycleService { const url = `/api/v3/cycles/${id}/?organization_id=${orgId}` return this._httpClient.delete(url).pipe( tap(() => { - this.get(orgId) + this.getCycles(orgId) }), catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error deleting cycle') diff --git a/src/@seed/api/dataset/dataset.service.ts b/src/@seed/api/dataset/dataset.service.ts index c6adb53b..440f2dd5 100644 --- a/src/@seed/api/dataset/dataset.service.ts +++ b/src/@seed/api/dataset/dataset.service.ts @@ -116,7 +116,6 @@ export class DatasetService { getImportFile(orgId: number, fieldId: number): Observable { const url = `/api/v3/import_files/${fieldId}/?organization_id=${orgId}` return this._httpClient.get(url).pipe( - tap((response) => { console.log('temp', response) }), map(({ import_file }) => import_file), catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error fetching import file') diff --git a/src/@seed/api/mapping/mapping.service.ts b/src/@seed/api/mapping/mapping.service.ts index 5ad9657b..ccfb7ac3 100644 --- a/src/@seed/api/mapping/mapping.service.ts +++ b/src/@seed/api/mapping/mapping.service.ts @@ -2,7 +2,7 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http' import { inject, Injectable } from '@angular/core' import { ErrorService } from '@seed/services' import { UserService } from '../user' -import { catchError, type Observable } from 'rxjs' +import { catchError, map, type Observable } from 'rxjs' import type { FirstFiveRowsResponse, MappingSuggestionsResponse, RawColumnNamesResponse } from './mapping.types' @Injectable({ providedIn: 'root' }) @@ -21,20 +21,22 @@ export class MappingService { ) } - rawColumnNames(orgId: number, importFileId: number): Observable { + rawColumnNames(orgId: number, importFileId: number): Observable { const url = `/api/v3/import_files/${importFileId}/raw_column_names/?organization_id=${orgId}` return this._httpClient.get(url) .pipe( + map(({ raw_columns }) => raw_columns ), catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error fetching raw column names') }), ) } - firstFiveRows(orgId: number, importFileId: number): Observable { + firstFiveRows(orgId: number, importFileId: number): Observable[]> { const url = `/api/v3/import_files/${importFileId}/first_five_rows/?organization_id=${orgId}` return this._httpClient.get(url) .pipe( + map(({ first_five_rows }) => first_five_rows), catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error fetching first five rows') }), diff --git a/src/@seed/api/mapping/mapping.types.ts b/src/@seed/api/mapping/mapping.types.ts index 45b8fad3..170a7d78 100644 --- a/src/@seed/api/mapping/mapping.types.ts +++ b/src/@seed/api/mapping/mapping.types.ts @@ -3,10 +3,12 @@ import type { Column } from '../column' export type MappingSuggestionsResponse = { status: string; property_columns: Column[]; - suggested_column_mappings: Record; + suggested_column_mappings: SuggestedColumnMapping; taxlot_columns: Column[]; } +export type SuggestedColumnMapping = Record + export type RawColumnNamesResponse = { status: string; raw_columns: string[]; diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.html b/src/app/modules/datasets/data-mappings/data-mapping.component.html index cd49eca1..1c7a7c64 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.html +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.html @@ -9,7 +9,7 @@ >
- + @@ -22,28 +22,44 @@ -
-

- HelpEmail Templates -

-

Custom Emails

-
- Custom emails can be sent to Building Owners using the templates defined below. The email will be sent to the SEED record's Owner - Email address and is currently not configurable. The email 'from' address is the same as the server email address which is also used - to email users their account information. -
-
- The email supports brace templating to pull in data from the SEED property record. For example, the snippet below will replace the - latitude and longitude from the SEED record. Other fields can be added, but make sure to use the SEED field name not the display name. -
-
- "Your building's latitude and longitude is {{ '{{' }}latitude{{ '}}' }}, {{ '{{' }}longitude{{ '}}' }}!" -
-
+
- MAPPING! + +
+
Cycle
+
{{ cycle?.name }}
+
+
+
Column Profile
+
none selected
+
+ + +
+ + +
+ Set all fields to + + Property + Tax Lot + +
+
+ + + + +
\ No newline at end of file diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.ts b/src/app/modules/datasets/data-mappings/data-mapping.component.ts index 3f467a34..fb2b0080 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.ts +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.ts @@ -5,12 +5,25 @@ import { MatButtonModule } from '@angular/material/button' import { MatIconModule } from '@angular/material/icon' import { MatSidenavModule } from '@angular/material/sidenav' import { ActivatedRoute } from '@angular/router' -import { DatasetService, ImportFile } from '@seed/api/dataset' -import { FirstFiveRowsResponse, MappingService, MappingSuggestionsResponse, RawColumnNamesResponse } from '@seed/api/mapping' -import { UserService } from '@seed/api/user' -import { PageComponent } from '@seed/components' import { AgGridAngular } from 'ag-grid-angular' +import type { ColDef, ColGroupDef, GridApi, GridReadyEvent, RowNode } from 'ag-grid-community' import { catchError, filter, forkJoin, of, Subject, switchMap, take, tap } from 'rxjs' +import type { ImportFile } from '@seed/api/dataset' +import { DatasetService } from '@seed/api/dataset' +import type { MappingSuggestionsResponse } from '@seed/api/mapping' +import { MappingService } from '@seed/api/mapping' +import { UserService } from '@seed/api/user' +import { PageComponent } from '@seed/components' +import { ConfigService } from '@seed/services' +import { HelpComponent } from './help.component' +import { Column } from '@seed/api/column' +import { Cycle } from '@seed/api/cycle' +import { CycleService } from '@seed/api/cycle/cycle.service' +import { InventoryDisplayType, Profile } from 'app/modules/inventory' +import { InventoryService } from '@seed/api/inventory' +import { MatDividerModule } from '@angular/material/divider' +import { MatSelectModule } from '@angular/material/select' + @Component({ selector: 'seed-data-mapping', @@ -18,25 +31,43 @@ import { catchError, filter, forkJoin, of, Subject, switchMap, take, tap } from imports: [ AgGridAngular, CommonModule, + HelpComponent, + MatButtonModule, + MatDividerModule, MatIconModule, - PageComponent, MatSidenavModule, - MatButtonModule, + MatSelectModule, + PageComponent, ], }) export class DataMappingComponent implements OnDestroy, OnInit { private readonly _unsubscribeAll$ = new Subject() + private _configService = inject(ConfigService) + private _cycleService = inject(CycleService) private _datasetService = inject(DatasetService) + private _inventoryService = inject(InventoryService) private _mappingService = inject(MappingService) private _router = inject(ActivatedRoute) private _userService = inject(UserService) - helpOpened = false + columns: Column[] + columnDefs: ColDef[] + currentProfile: Profile + cycle: Cycle + defaultInventoryType = 'Property' fileId = this._router.snapshot.params.id as number + firstFiveRows: Record[] + helpOpened = false importFile: ImportFile - orgId: number - firstFiveRows: FirstFiveRowsResponse + gridApi: GridApi + gridOptions = { + singleClickEdit: true, + suppressMovableColumns: true, + } + gridTheme$ = this._configService.gridTheme$ mappingSuggestions: MappingSuggestionsResponse - rawColumnNames: RawColumnNamesResponse + orgId: number + rawColumnNames: string[] = [] + rowData: Record[] = [] ngOnInit(): void { console.log('Data Mapping Component Initialized') @@ -47,6 +78,7 @@ export class DataMappingComponent implements OnDestroy, OnInit { switchMap(() => this.getImportFile()), filter(Boolean), switchMap(() => this.getMappingData()), + tap(() => { this.setGrid() }), ) .subscribe() } @@ -57,20 +89,21 @@ export class DataMappingComponent implements OnDestroy, OnInit { take(1), tap((importFile) => { this.importFile = importFile }), catchError(() => { - console.log('bad importfile') return of(null) }), ) } getMappingData() { return forkJoin([ + this._cycleService.getCycle(this.orgId, this.importFile.cycle), this._mappingService.firstFiveRows(this.orgId, this.fileId), this._mappingService.mappingSuggestions(this.orgId, this.fileId), this._mappingService.rawColumnNames(this.orgId, this.fileId), ]) .pipe( take(1), - tap(([firstFiveRows, mappingSuggestions, rawColumnNames]) => { + tap(([cycle, firstFiveRows, mappingSuggestions, rawColumnNames]) => { + this.cycle = cycle this.firstFiveRows = firstFiveRows this.mappingSuggestions = mappingSuggestions this.rawColumnNames = rawColumnNames @@ -78,6 +111,113 @@ export class DataMappingComponent implements OnDestroy, OnInit { ) } + setGrid() { + this.setColumnDefs() + this.setRowData() + } + + setColumnDefs() { + const seedCols: ColDef[] = [ + { + field: 'omit', + headerName: 'Omit', + editable: true, + cellEditor: 'agCheckboxCellEditor', + }, + { + field: 'inventory_type', + headerName: 'Inventory Type', + editable: true, + cellEditor: 'agSelectCellEditor', + cellEditorParams: { + values: ['Property', 'Tax Lot'], + }, + cellRenderer: this.dropdownRenderer, + }, + { + field: 'seed_header', + headerName: 'SEED Header', + editable: true, + cellRenderer: this.inputRenderer, + cellEditor: 'agTextCellEditor', + }, + ] + + const fileCols: ColDef[] = [ + { field: 'data_type', headerName: 'Data Type' }, + { field: 'units', headerName: 'Units' }, + { field: 'file_header', headerName: 'Data File Header' }, + { field: 'row1', headerName: 'Row 1' }, + { field: 'row2', headerName: 'Row 2' }, + { field: 'row3', headerName: 'Row 3' }, + { field: 'row4', headerName: 'Row 4' }, + { field: 'row5', headerName: 'Row 5' }, + ] + + this.columnDefs = [ + { headerName: 'SEED', children: seedCols } as ColGroupDef, + { headerName: this.importFile.uploaded_filename, children: fileCols } as ColGroupDef, + ] + } + + dropdownRenderer({ value }: { value: string }) { + return ` +
+ ${value ?? ''} + arrow_drop_down +
+ ` + } + + inputRenderer({ value }: { value: string }) { + return ` +
+ ${value ?? ''} +
+ ` + } + + setRowData() { + this.rowData = [] + + // transpose first 5 rows to fit into the grid + for (const header of this.rawColumnNames) { + const keys = ['file_header', 'row1', 'row2', 'row3', 'row4', 'row5'] + const values = this.firstFiveRows.map((r) => r[header]) + values.unshift(header) + + const data = Object.fromEntries(keys.map((k, i) => [k, values[i]])) + this.rowData.push(data) + } + + for (const row of this.rowData) { + row.omit = false + } + } + + onGridReady(agGrid: GridReadyEvent) { + this.gridApi = agGrid.api + // this.gridApi.addEventListener('cellClicked', this.onCellClicked.bind(this) as (event: CellClickedEvent) => void) + } + + setAllInventoryType(value: InventoryDisplayType) { + this.defaultInventoryType = value + this.gridApi.forEachNode((n) => n.setDataValue('inventory_type', value)) + } + + copyHeadersToSeed() { + const { property_columns, taxlot_columns, suggested_column_mappings } = this.mappingSuggestions + const columns = this.defaultInventoryType === 'Tax Lot' ? taxlot_columns : property_columns + const columnMap: Record = columns.reduce((acc, { column_name, display_name }) => ({ ...acc, [column_name]: display_name }), {}) + + this.gridApi.forEachNode((n: RowNode<{ file_header: string }>) => { + const fileHeader = n.data.file_header + const suggestedColumnName = suggested_column_mappings[fileHeader][1] + const displayName = columnMap[suggestedColumnName] + n.setDataValue('seed_header', displayName) + }) + } + toggleHelp = () => { this.helpOpened = !this.helpOpened } diff --git a/src/app/modules/datasets/dataset/dataset.component.ts b/src/app/modules/datasets/dataset/dataset.component.ts index 5e4bfdea..480108eb 100644 --- a/src/app/modules/datasets/dataset/dataset.component.ts +++ b/src/app/modules/datasets/dataset/dataset.component.ts @@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common' import type { OnDestroy, OnInit } from '@angular/core' import { Component, inject } from '@angular/core' import { MatDialog } from '@angular/material/dialog' -import { ActivatedRoute } from '@angular/router' +import { ActivatedRoute, Router } from '@angular/router' import { AgGridAngular } from 'ag-grid-angular' import type { CellClickedEvent, ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' import type { Observable } from 'rxjs' @@ -31,6 +31,7 @@ export class DatasetComponent implements OnDestroy, OnInit { private _datasetService = inject(DatasetService) private _dialog = inject(MatDialog) private _route = inject(ActivatedRoute) + private _router = inject(Router) private _userService = inject(UserService) private readonly _unsubscribeAll$ = new Subject() columnDefs: ColDef[] = [] @@ -128,6 +129,7 @@ export class DatasetComponent implements OnDestroy, OnInit { } else if (action === 'download') { this.downloadDocument(importFile.file, importFile.uploaded_filename) } else if (action === 'dataMapping') { + void this._router.navigate(['/data/mappings/', importFile.id]) console.log('data mapping', importFile) } else if (action === 'dataPairing') { console.log('data pairing', importFile) diff --git a/src/app/modules/inventory-detail/sensors/sensors.component.ts b/src/app/modules/inventory-detail/sensors/sensors.component.ts index 76351d01..c66bf664 100644 --- a/src/app/modules/inventory-detail/sensors/sensors.component.ts +++ b/src/app/modules/inventory-detail/sensors/sensors.component.ts @@ -85,9 +85,6 @@ export class SensorsComponent implements OnDestroy, OnInit { tap((orgId) => { this.orgId = orgId }), - tap(() => { - this._cycleService.get(this.orgId) - }), switchMap(() => this._cycleService.cycles$), tap((cycles) => { this.cycleId = cycles.length ? cycles[0].id : null }), switchMap(() => this._datasetService.datasets$), diff --git a/src/app/modules/inventory-list/list/inventory.component.ts b/src/app/modules/inventory-list/list/inventory.component.ts index 5c188bf0..508e3b07 100644 --- a/src/app/modules/inventory-list/list/inventory.component.ts +++ b/src/app/modules/inventory-list/list/inventory.component.ts @@ -151,7 +151,7 @@ export class InventoryComponent implements OnDestroy, OnInit { */ getDependencies(org_id: number) { this.orgId = org_id - this._cycleService.get(this.orgId) + // this._cycleService.getCycles(this.orgId) return combineLatest([ this._userService.currentUser$, diff --git a/src/main.ts b/src/main.ts index 14c1329d..9add0539 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,15 +1,19 @@ import { bootstrapApplication } from '@angular/platform-browser' -import { ClientSideRowModelModule, ColumnAutoSizeModule, EventApiModule, ModuleRegistry, PaginationModule, ValidationModule } from 'ag-grid-community' +import { CheckboxEditorModule, ClientSideRowModelModule, ColumnAutoSizeModule, EventApiModule, ModuleRegistry, PaginationModule, RowApiModule, SelectEditorModule, TextEditorModule, ValidationModule } from 'ag-grid-community' import { AppComponent } from 'app/app.component' import { appConfig } from 'app/app.config' // TEMP - should be overwritten when analyses pr is merged ModuleRegistry.registerModules([ + CheckboxEditorModule, ClientSideRowModelModule, ColumnAutoSizeModule, EventApiModule, PaginationModule, + RowApiModule, + SelectEditorModule, + TextEditorModule, ValidationModule, ]) From 1b9c0fb1e129f22be5f6cdc677a25220bbe3d817 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Fri, 27 Jun 2025 22:01:58 +0000 Subject: [PATCH 14/37] debug --- src/app/modules/inventory-list/list/inventory.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/modules/inventory-list/list/inventory.component.ts b/src/app/modules/inventory-list/list/inventory.component.ts index 508e3b07..d4d890a9 100644 --- a/src/app/modules/inventory-list/list/inventory.component.ts +++ b/src/app/modules/inventory-list/list/inventory.component.ts @@ -151,7 +151,7 @@ export class InventoryComponent implements OnDestroy, OnInit { */ getDependencies(org_id: number) { this.orgId = org_id - // this._cycleService.getCycles(this.orgId) + this._cycleService.getCycles(this.orgId) return combineLatest([ this._userService.currentUser$, From 4a270eab042316038a1dbf3fae716a84b5541ad5 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Mon, 30 Jun 2025 20:30:18 +0000 Subject: [PATCH 15/37] mapping table fxns and styles --- .../ag-grid/autocomplete.component.html | 16 ++ .../ag-grid/autocomplete.component.ts | 60 +++++++ .../ag-grid/editHeader.component.ts | 17 ++ src/@seed/components/ag-grid/index.ts | 1 + src/@seed/components/index.ts | 1 + src/@seed/utils/string-matching.util.ts | 12 ++ src/app/ag-grid-modules.ts | 30 ++++ .../datasets/data-mappings/column-defs.ts | 156 +++++++++++++++++ .../datasets/data-mappings/constants.ts | 57 +++++++ .../data-mappings/data-mapping.component.html | 15 +- .../data-mappings/data-mapping.component.ts | 159 +++++++++--------- .../inventory-list/map/labels.component.ts | 14 +- src/main.ts | 14 +- 13 files changed, 441 insertions(+), 111 deletions(-) create mode 100644 src/@seed/components/ag-grid/autocomplete.component.html create mode 100644 src/@seed/components/ag-grid/autocomplete.component.ts create mode 100644 src/@seed/components/ag-grid/editHeader.component.ts create mode 100644 src/@seed/components/ag-grid/index.ts create mode 100644 src/@seed/utils/string-matching.util.ts create mode 100644 src/app/ag-grid-modules.ts create mode 100644 src/app/modules/datasets/data-mappings/column-defs.ts create mode 100644 src/app/modules/datasets/data-mappings/constants.ts diff --git a/src/@seed/components/ag-grid/autocomplete.component.html b/src/@seed/components/ag-grid/autocomplete.component.html new file mode 100644 index 00000000..db0592f0 --- /dev/null +++ b/src/@seed/components/ag-grid/autocomplete.component.html @@ -0,0 +1,16 @@ + + + + @for (option of filteredOptions; track $index) { + + {{ option }} + + } + + \ No newline at end of file diff --git a/src/@seed/components/ag-grid/autocomplete.component.ts b/src/@seed/components/ag-grid/autocomplete.component.ts new file mode 100644 index 00000000..1b7e7f11 --- /dev/null +++ b/src/@seed/components/ag-grid/autocomplete.component.ts @@ -0,0 +1,60 @@ +import type { AfterViewInit, ElementRef } from '@angular/core' +import { Component, ViewChild } from '@angular/core' +import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms' +import { MatAutocompleteModule } from '@angular/material/autocomplete' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatInputModule } from '@angular/material/input' +import type { ICellEditorAngularComp } from 'ag-grid-angular' +import type { ICellEditorParams } from 'ag-grid-community' +import { isOrderedSubset } from '@seed/utils/string-matching.util' + +@Component({ + selector: 'seed-ag-grid-auto-complete-cell', + templateUrl: './autocomplete.component.html', + imports: [ + FormsModule, + MatAutocompleteModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule, + ], +}) +export class AutocompleteCellComponent implements ICellEditorAngularComp, AfterViewInit { + @ViewChild('input') input!: ElementRef + inputCtrl = new FormControl('') + filteredOptions: string[] = [] + + params!: unknown + options: string[] = [] + + agInit(params: ICellEditorParams): void { + this.params = params + this.options = ((params as unknown) as { values: string[] }).values || [] + this.inputCtrl.setValue(params.value as string) + this.filteredOptions = [...this.options] + this.inputCtrl.valueChanges.subscribe((value) => { + // autocomplete + this.filteredOptions = this.options.filter((option) => { + return isOrderedSubset(value, option) + }) + }) + } + + getValue() { + return this.inputCtrl.value + } + + ngAfterViewInit(): void { + setTimeout(() => { + this.input.nativeElement.focus() + }) + } + + onKeyDown(event: KeyboardEvent) { + // if enter or tab, accept the value and stop propagation + const exitKeys = ['Enter', 'Tab'] + if (!exitKeys.includes(event.key)) { + event.stopPropagation() + } + } +} diff --git a/src/@seed/components/ag-grid/editHeader.component.ts b/src/@seed/components/ag-grid/editHeader.component.ts new file mode 100644 index 00000000..7facf366 --- /dev/null +++ b/src/@seed/components/ag-grid/editHeader.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core' + +@Component({ + selector: 'seed-edit-header', + template: ` +
+ {{ name }} + edit +
+ `, +}) +export class EditHeaderComponent { + name: string + agInit(params: { name: string }): void { + this.name = params.name + } +} diff --git a/src/@seed/components/ag-grid/index.ts b/src/@seed/components/ag-grid/index.ts new file mode 100644 index 00000000..82292220 --- /dev/null +++ b/src/@seed/components/ag-grid/index.ts @@ -0,0 +1 @@ +export * from './editHeader.component' \ No newline at end of file diff --git a/src/@seed/components/index.ts b/src/@seed/components/index.ts index 9f6aab18..6cebd7eb 100644 --- a/src/@seed/components/index.ts +++ b/src/@seed/components/index.ts @@ -4,6 +4,7 @@ export * from './card' export * from './clipboard' export * from './delete-modal' export * from './drawer' +export * from './ag-grid' export * from './label' export * from './loading-bar' export * from './masonry' diff --git a/src/@seed/utils/string-matching.util.ts b/src/@seed/utils/string-matching.util.ts new file mode 100644 index 00000000..46ee1a56 --- /dev/null +++ b/src/@seed/utils/string-matching.util.ts @@ -0,0 +1,12 @@ +/** + * Returns true if all characters in `input` appear in `target` in the same order (not necessarily consecutively). + * Used for fuzzy matching like 'ac' matching 'abc' but not 'cab'. + */ +export const isOrderedSubset = (input: string, target: string): boolean => { + let i = 0 + for (const char of target.toLowerCase()) { + if (char === input[i]?.toLowerCase()) i++ + if (i === input.length) return true + } + return i === input.length +} diff --git a/src/app/ag-grid-modules.ts b/src/app/ag-grid-modules.ts new file mode 100644 index 00000000..da988e0f --- /dev/null +++ b/src/app/ag-grid-modules.ts @@ -0,0 +1,30 @@ +import { + CellStyleModule, + CheckboxEditorModule, + ClientSideRowModelModule, + ColumnAutoSizeModule, + CustomEditorModule, + EventApiModule, + ModuleRegistry, + PaginationModule, + RenderApiModule, + RowApiModule, + SelectEditorModule, + TextEditorModule, + ValidationModule, +} from 'ag-grid-community' + +ModuleRegistry.registerModules([ + CellStyleModule, + CheckboxEditorModule, + ClientSideRowModelModule, + ColumnAutoSizeModule, + CustomEditorModule, + EventApiModule, + PaginationModule, + RenderApiModule, + RowApiModule, + SelectEditorModule, + TextEditorModule, + ValidationModule, +]) diff --git a/src/app/modules/datasets/data-mappings/column-defs.ts b/src/app/modules/datasets/data-mappings/column-defs.ts new file mode 100644 index 00000000..de97fea3 --- /dev/null +++ b/src/app/modules/datasets/data-mappings/column-defs.ts @@ -0,0 +1,156 @@ +import { EditHeaderComponent } from '@seed/components' +import { AutocompleteCellComponent } from '@seed/components/ag-grid/autocomplete.component' +import type { CellValueChangedEvent, ColDef, ColGroupDef, ICellRendererParams } from 'ag-grid-community' +import { dataTypeOptions, unitMap } from './constants' + +export const gridOptions = { + singleClickEdit: true, + suppressMovableColumns: true, + // defaultColDef: { cellClass: (params: CellClassParams) => params.colDef.editable ? 'bg-primary bg-opacity-25' : '' }, +} + +// Special cases +const canEdit = (dataType: string, field: string, isNewColumn: boolean): boolean => { + const editMap: Record = { + dataType: isNewColumn, + inventory_type: true, + units: ['EUI', 'Area', 'GHG', 'GHG Intensity', 'Water use', 'WUI'].includes(dataType), + } + + return editMap[field] +} + +const dropdownRenderer = (params: ICellRendererParams) => { + const value = params.value as string + const data = params.data as { dataType: string; isNewColumn: boolean } + const field = params.colDef.field + + if (!canEdit(data.dataType, field, data.isNewColumn)) { + return value + } + + return ` +
+ ${value ?? ''} + arrow_drop_down +
+ ` +} + +const canEditClass = 'bg-primary bg-opacity-25 rounded' + +export const buildColumnDefs = ( + columnNames: string[], + uploadedFilename: string, + seedHeaderChange: (event: CellValueChangedEvent) => void, + dataTypeChange: (event: CellValueChangedEvent) => void, +): (ColDef | ColGroupDef)[] => { + const seedCols: ColDef[] = [ + { field: 'isExtraData', hide: true }, + { field: 'isNewColumn', hide: true }, + // OMIT + { + field: 'omit', + headerName: 'Omit', + cellEditor: 'agCheckboxCellEditor', + editable: true, + width: 70, + }, + { + field: 'inventory_type', + headerName: 'Inventory Type', + headerComponent: EditHeaderComponent, + headerComponentParams: { + name: 'Inventory Type', + }, + cellEditor: 'agSelectCellEditor', + cellEditorParams: { + values: ['Property', 'Tax Lot'], + }, + cellRenderer: dropdownRenderer, + editable: true, + cellClass: canEditClass, + }, + // SEED HEADER + { + field: 'seed_header', + headerName: 'SEED Header', + cellEditor: AutocompleteCellComponent, + cellEditorParams: { + values: columnNames, + }, + headerComponent: EditHeaderComponent, + headerComponentParams: { + name: 'SEED Header', + }, + onCellValueChanged: seedHeaderChange, + editable: true, + cellClass: canEditClass, + }, + ] + + const fileCols: ColDef[] = [ + // DATA TYPE: Editable if isExtraData is true + { + field: 'dataType', + headerName: 'Data Type', + headerComponent: EditHeaderComponent, + headerComponentParams: { + name: 'Data Type', + }, + cellEditor: 'agSelectCellEditor', + cellEditorParams: { + values: dataTypeOptions, + }, + cellRenderer: dropdownRenderer, + editable: (params) => { + const data = params?.data as { isNewColumn: boolean } + return canEdit(null, 'dataType', data.isNewColumn) + }, + onCellValueChanged: dataTypeChange, + cellClass: (params) => { + const data = params?.data as { isNewColumn: boolean } + return canEdit(null, 'dataType', data.isNewColumn) ? canEditClass : '' + }, + }, + /* UNITS: Only editable for Area, EUI, GHG, GHGI, Water use, WUI + * Dropdowns are populated based on a unit type map + */ + { + field: 'units', + headerName: 'Units', + headerComponent: EditHeaderComponent, + headerComponentParams: { + name: 'Units', + }, + cellEditor: 'agSelectCellEditor', + cellEditorParams: ({ data }: { data: { dataType: string } }) => { + return { + values: unitMap[data.dataType] ?? [], + } + }, + cellRenderer: dropdownRenderer, + editable: (params) => { + const data = params?.data as { dataType: string } + return canEdit(data.dataType, 'units', null) + }, + cellClass: (params) => { + const data = params?.data as { dataType: string } + return canEdit(data.dataType, 'units', null) ? canEditClass : '' + }, + }, + { field: 'file_header', headerName: 'Data File Header' }, + { field: 'row1', headerName: 'Row 1' }, + { field: 'row2', headerName: 'Row 2' }, + { field: 'row3', headerName: 'Row 3' }, + { field: 'row4', headerName: 'Row 4' }, + { field: 'row5', headerName: 'Row 5' }, + ] + + const columnDefs = [ + { headerName: 'SEED', children: seedCols } as ColGroupDef, + { headerName: uploadedFilename, children: fileCols } as ColGroupDef, + ] + + return columnDefs +} diff --git a/src/app/modules/datasets/data-mappings/constants.ts b/src/app/modules/datasets/data-mappings/constants.ts new file mode 100644 index 00000000..51dded44 --- /dev/null +++ b/src/app/modules/datasets/data-mappings/constants.ts @@ -0,0 +1,57 @@ +export const dataTypeMap: Record = { + None: { display: 'None', units: null }, + number: { display: 'Number', units: null }, + integer: { display: 'Integer', units: null }, + string: { display: 'Text', units: null }, + datetime: { display: 'Datetime', units: null }, + date: { display: 'Date', units: null }, + boolean: { display: 'Boolean', units: null }, + area: { display: 'Area', units: 'ft²' }, + eui: { display: 'EUI', units: 'kBtu/ft²/year' }, + geometry: { display: 'Geometry', units: null }, + ghg: { display: 'GHG', units: 'MtC02e/year' }, + ghg_intensity: { display: 'GHG Intensity', units: 'kgCO2e/ft²/year' }, + // water_use: { display: 'Water Use', units: 'kgal/year' }, + // wui: { display: 'Water Use Intensity', units: 'gal/ft²/year' }, +} + +export const unitMap: Record = { + Area: ['ft²', 'm²'], + EUI: [ + 'kBtu/ft²/year', + 'kWh/m²/year', + 'GJ/m²/year', + 'MJ/m²/year', + 'kBtu/m²/year', + ], + GHG: ['MtCO2e/year', 'kgCO2e/year'], + 'GHG Intensity': [ + 'MtCO2e/ft²/year', + 'kgCO2e/ft²/year', + 'MtCO2e/m²/year', + 'kgCO2e/m²/year', + ], + 'Water Use': ['kgal/year', 'gal/year', 'L/year'], + 'Water Use Intensity': [ + 'kgal/ft²/year', + 'gal/ft²/year', + 'L/m²/year', + ], +} + +export const dataTypeOptions = [ + 'None', + 'Number', + 'Integer', + 'Text', + 'Datetime', + 'Date', + 'Boolean', + 'Area', + 'EUI', + 'Geometry', + 'GHG', + 'GHG Intensity', + 'Water Use', + 'Water Use Intensity', +] diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.html b/src/app/modules/datasets/data-mappings/data-mapping.component.html index 1c7a7c64..6d41d6a8 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.html +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.html @@ -41,16 +41,17 @@
-
- Set all fields to - - Property - Tax Lot - -
+ + Property Types + Tax Lot Types +
+
+
+
Editable Cell
+
() private _configService = inject(ConfigService) private _cycleService = inject(CycleService) + private _columnService = inject(ColumnService) private _datasetService = inject(DatasetService) private _inventoryService = inject(InventoryService) private _mappingService = inject(MappingService) private _router = inject(ActivatedRoute) private _userService = inject(UserService) columns: Column[] + columnNames: string[] + columnMap: Record columnDefs: ColDef[] currentProfile: Profile cycle: Cycle - defaultInventoryType = 'Property' + defaultInventoryType: InventoryDisplayType = 'Property' + defaultRow: Record fileId = this._router.snapshot.params.id as number firstFiveRows: Record[] helpOpened = false importFile: ImportFile gridApi: GridApi - gridOptions = { - singleClickEdit: true, - suppressMovableColumns: true, - } + gridOptions = gridOptions gridTheme$ = this._configService.gridTheme$ mappingSuggestions: MappingSuggestionsResponse orgId: number @@ -70,7 +77,6 @@ export class DataMappingComponent implements OnDestroy, OnInit { rowData: Record[] = [] ngOnInit(): void { - console.log('Data Mapping Component Initialized') this._userService.currentOrganizationId$ .pipe( take(1), @@ -107,74 +113,31 @@ export class DataMappingComponent implements OnDestroy, OnInit { this.firstFiveRows = firstFiveRows this.mappingSuggestions = mappingSuggestions this.rawColumnNames = rawColumnNames + this.setColumns() }), ) } setGrid() { + this.defaultRow = { + isExtraData: false, + omit: null, + seed_header: null, + inventory_type: this.defaultInventoryType, + dataType: null, + units: null, + } this.setColumnDefs() this.setRowData() } setColumnDefs() { - const seedCols: ColDef[] = [ - { - field: 'omit', - headerName: 'Omit', - editable: true, - cellEditor: 'agCheckboxCellEditor', - }, - { - field: 'inventory_type', - headerName: 'Inventory Type', - editable: true, - cellEditor: 'agSelectCellEditor', - cellEditorParams: { - values: ['Property', 'Tax Lot'], - }, - cellRenderer: this.dropdownRenderer, - }, - { - field: 'seed_header', - headerName: 'SEED Header', - editable: true, - cellRenderer: this.inputRenderer, - cellEditor: 'agTextCellEditor', - }, - ] - - const fileCols: ColDef[] = [ - { field: 'data_type', headerName: 'Data Type' }, - { field: 'units', headerName: 'Units' }, - { field: 'file_header', headerName: 'Data File Header' }, - { field: 'row1', headerName: 'Row 1' }, - { field: 'row2', headerName: 'Row 2' }, - { field: 'row3', headerName: 'Row 3' }, - { field: 'row4', headerName: 'Row 4' }, - { field: 'row5', headerName: 'Row 5' }, - ] - - this.columnDefs = [ - { headerName: 'SEED', children: seedCols } as ColGroupDef, - { headerName: this.importFile.uploaded_filename, children: fileCols } as ColGroupDef, - ] - } - - dropdownRenderer({ value }: { value: string }) { - return ` -
- ${value ?? ''} - arrow_drop_down -
- ` - } - - inputRenderer({ value }: { value: string }) { - return ` -
- ${value ?? ''} -
- ` + this.columnDefs = buildColumnDefs( + this.columnNames, + this.importFile.uploaded_filename, + this.seedHeaderChange.bind(this), + this.dataTypeChange.bind(this), + ) } setRowData() { @@ -182,11 +145,11 @@ export class DataMappingComponent implements OnDestroy, OnInit { // transpose first 5 rows to fit into the grid for (const header of this.rawColumnNames) { - const keys = ['file_header', 'row1', 'row2', 'row3', 'row4', 'row5'] + const keys = ['row1', 'row2', 'row3', 'row4', 'row5'] const values = this.firstFiveRows.map((r) => r[header]) - values.unshift(header) + const rows: Record = Object.fromEntries(keys.map((k, i) => [k, values[i]])) - const data = Object.fromEntries(keys.map((k, i) => [k, values[i]])) + const data = { ...rows, ...this.defaultRow, file_header: header } this.rowData.push(data) } @@ -203,6 +166,44 @@ export class DataMappingComponent implements OnDestroy, OnInit { setAllInventoryType(value: InventoryDisplayType) { this.defaultInventoryType = value this.gridApi.forEachNode((n) => n.setDataValue('inventory_type', value)) + this.setColumns() + } + + setColumns() { + this.columns = this.defaultInventoryType === 'Tax Lot' ? this.mappingSuggestions?.taxlot_columns : this.mappingSuggestions?.property_columns + this.columnNames = this.columns.map((c) => c.display_name) + this.columnMap = this.columns.reduce((acc, curr) => ({ ...acc, [curr.display_name]: curr }), {}) + } + + seedHeaderChange = (params: CellValueChangedEvent): void => { + const node = params.node as RowNode + const newValue = params.newValue as string + const column = this.columnMap[newValue] ?? null + + const dataTypeConfig = dataTypeMap[column?.data_type] ?? { display: 'None', units: null } + + node.setData({ + ...node.data, + isNewColumn: !column, + isExtraData: column?.is_extra_data ?? true, + dataType: dataTypeConfig.display, + units: dataTypeConfig.units, + }) + + this.refreshNode(node) + } + + dataTypeChange = (params: CellValueChangedEvent): void => { + const node = params.node as RowNode + node.setDataValue('units', null) + this.refreshNode(node) + } + + refreshNode(node: RowNode) { + this.gridApi.refreshCells({ + rowNodes: [node], + force: true, + }) } copyHeadersToSeed() { @@ -210,11 +211,11 @@ export class DataMappingComponent implements OnDestroy, OnInit { const columns = this.defaultInventoryType === 'Tax Lot' ? taxlot_columns : property_columns const columnMap: Record = columns.reduce((acc, { column_name, display_name }) => ({ ...acc, [column_name]: display_name }), {}) - this.gridApi.forEachNode((n: RowNode<{ file_header: string }>) => { - const fileHeader = n.data.file_header + this.gridApi.forEachNode((node: RowNode<{ file_header: string }>) => { + const fileHeader = node.data.file_header const suggestedColumnName = suggested_column_mappings[fileHeader][1] const displayName = columnMap[suggestedColumnName] - n.setDataValue('seed_header', displayName) + node.setDataValue('seed_header', displayName) }) } diff --git a/src/app/modules/inventory-list/map/labels.component.ts b/src/app/modules/inventory-list/map/labels.component.ts index c90ee5bc..6c0219c9 100644 --- a/src/app/modules/inventory-list/map/labels.component.ts +++ b/src/app/modules/inventory-list/map/labels.component.ts @@ -11,6 +11,7 @@ import { MatSelectModule } from '@angular/material/select' import type { Label, LabelOperator } from '@seed/api/label' import { OrganizationService } from '@seed/api/organization' import type { CurrentUser } from '@seed/api/user' +import { isOrderedSubset } from '@seed/utils/string-matching.util' @Component({ selector: 'seed-inventory-list-map-labels', @@ -84,7 +85,7 @@ export class LabelsComponent implements OnChanges { labelInputChange(event: Event) { const value = (event.target as HTMLInputElement).value - this.filteredLabels = this.labels.filter((label) => this.isOrderedSubset(value.toLowerCase(), label.name.toLowerCase())) + this.filteredLabels = this.labels.filter((label) => isOrderedSubset(value, label.name)) } onLabelChange() { @@ -99,15 +100,4 @@ export class LabelsComponent implements OnChanges { .updateOrganizationUser(this.currentUser.org_user_id, this.currentUser.org_id, this.currentUser.settings) .subscribe() } - - // determine if a string is a subset of another string, preserving order - // e.g. 'ac' is a subset of 'abc' - isOrderedSubset(input: string, target: string): boolean { - let i = 0 - for (const char of target) { - if (char === input[i]) i++ - if (i === input.length) return true - } - return i === input.length - } } diff --git a/src/main.ts b/src/main.ts index df8740f0..2166129e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,19 +1,7 @@ import { bootstrapApplication } from '@angular/platform-browser' -import { CheckboxEditorModule, ClientSideRowModelModule, ColumnAutoSizeModule, EventApiModule, ModuleRegistry, PaginationModule, RowApiModule, SelectEditorModule, TextEditorModule, ValidationModule } from 'ag-grid-community' import { AppComponent } from 'app/app.component' import { appConfig } from 'app/app.config' - -ModuleRegistry.registerModules([ - CheckboxEditorModule, - ClientSideRowModelModule, - ColumnAutoSizeModule, - EventApiModule, - PaginationModule, - RowApiModule, - SelectEditorModule, - TextEditorModule, - ValidationModule, -]) +import 'app/ag-grid-modules' bootstrapApplication(AppComponent, appConfig).catch((err: unknown) => { console.error(err) From c9fc08136d1a483b2e7e5d967ac334112a592a11 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Tue, 1 Jul 2025 14:55:01 +0000 Subject: [PATCH 16/37] validate data mapping --- src/@seed/api/dataset/dataset.types.ts | 12 +++ .../ag-grid/autocomplete.component.ts | 4 +- .../datasets/data-mappings/column-defs.ts | 39 +++---- .../datasets/data-mappings/constants.ts | 9 +- .../data-mappings/data-mapping.component.html | 12 ++- .../data-mappings/data-mapping.component.ts | 100 +++++++++++++++--- 6 files changed, 134 insertions(+), 42 deletions(-) diff --git a/src/@seed/api/dataset/dataset.types.ts b/src/@seed/api/dataset/dataset.types.ts index 17003a6d..cf1cf6ba 100644 --- a/src/@seed/api/dataset/dataset.types.ts +++ b/src/@seed/api/dataset/dataset.types.ts @@ -52,3 +52,15 @@ export type ImportFileResponse = { status: 'success'; import_file: ImportFile; } + +export type DataMappingRow = { + from_field: string; + from_units: string | null; + to_data_type: string | null; + to_field: string | null; + to_field_display_name: string | null; + to_table_name: string | null; + omit?: boolean; // optional, used for omitting columns + isExtraData?: boolean; // used internally, not part of the API + isNewColumn?: boolean; // used internally, not part of the API +} diff --git a/src/@seed/components/ag-grid/autocomplete.component.ts b/src/@seed/components/ag-grid/autocomplete.component.ts index 1b7e7f11..1cfa9db2 100644 --- a/src/@seed/components/ag-grid/autocomplete.component.ts +++ b/src/@seed/components/ag-grid/autocomplete.component.ts @@ -51,8 +51,8 @@ export class AutocompleteCellComponent implements ICellEditorAngularComp, AfterV } onKeyDown(event: KeyboardEvent) { - // if enter or tab, accept the value and stop propagation - const exitKeys = ['Enter', 'Tab'] + // if enter, accept the value and stop propagation + const exitKeys = ['Enter'] if (!exitKeys.includes(event.key)) { event.stopPropagation() } diff --git a/src/app/modules/datasets/data-mappings/column-defs.ts b/src/app/modules/datasets/data-mappings/column-defs.ts index de97fea3..86113131 100644 --- a/src/app/modules/datasets/data-mappings/column-defs.ts +++ b/src/app/modules/datasets/data-mappings/column-defs.ts @@ -10,11 +10,11 @@ export const gridOptions = { } // Special cases -const canEdit = (dataType: string, field: string, isNewColumn: boolean): boolean => { +const canEdit = (to_data_type: string, field: string, isNewColumn: boolean): boolean => { const editMap: Record = { - dataType: isNewColumn, - inventory_type: true, - units: ['EUI', 'Area', 'GHG', 'GHG Intensity', 'Water use', 'WUI'].includes(dataType), + to_data_type: isNewColumn, + to_table_name: true, + from_units: ['EUI', 'Area', 'GHG', 'GHG Intensity', 'Water use', 'WUI'].includes(to_data_type), } return editMap[field] @@ -22,10 +22,10 @@ const canEdit = (dataType: string, field: string, isNewColumn: boolean): boolean const dropdownRenderer = (params: ICellRendererParams) => { const value = params.value as string - const data = params.data as { dataType: string; isNewColumn: boolean } + const data = params.data as { to_data_type: string; isNewColumn: boolean } const field = params.colDef.field - if (!canEdit(data.dataType, field, data.isNewColumn)) { + if (!canEdit(data.to_data_type, field, data.isNewColumn)) { return value } @@ -48,6 +48,7 @@ export const buildColumnDefs = ( const seedCols: ColDef[] = [ { field: 'isExtraData', hide: true }, { field: 'isNewColumn', hide: true }, + { field: 'to_field', hide: true }, // OMIT { field: 'omit', @@ -57,7 +58,7 @@ export const buildColumnDefs = ( width: 70, }, { - field: 'inventory_type', + field: 'to_table_name', headerName: 'Inventory Type', headerComponent: EditHeaderComponent, headerComponentParams: { @@ -73,7 +74,7 @@ export const buildColumnDefs = ( }, // SEED HEADER { - field: 'seed_header', + field: 'to_field_display_name', headerName: 'SEED Header', cellEditor: AutocompleteCellComponent, cellEditorParams: { @@ -92,7 +93,7 @@ export const buildColumnDefs = ( const fileCols: ColDef[] = [ // DATA TYPE: Editable if isExtraData is true { - field: 'dataType', + field: 'to_data_type', headerName: 'Data Type', headerComponent: EditHeaderComponent, headerComponentParams: { @@ -105,41 +106,41 @@ export const buildColumnDefs = ( cellRenderer: dropdownRenderer, editable: (params) => { const data = params?.data as { isNewColumn: boolean } - return canEdit(null, 'dataType', data.isNewColumn) + return canEdit(null, 'to_data_type', data.isNewColumn) }, onCellValueChanged: dataTypeChange, cellClass: (params) => { const data = params?.data as { isNewColumn: boolean } - return canEdit(null, 'dataType', data.isNewColumn) ? canEditClass : '' + return canEdit(null, 'to_data_type', data.isNewColumn) ? canEditClass : '' }, }, /* UNITS: Only editable for Area, EUI, GHG, GHGI, Water use, WUI * Dropdowns are populated based on a unit type map */ { - field: 'units', + field: 'from_units', headerName: 'Units', headerComponent: EditHeaderComponent, headerComponentParams: { name: 'Units', }, cellEditor: 'agSelectCellEditor', - cellEditorParams: ({ data }: { data: { dataType: string } }) => { + cellEditorParams: ({ data }: { data: { to_data_type: string } }) => { return { - values: unitMap[data.dataType] ?? [], + values: unitMap[data.to_data_type] ?? [], } }, cellRenderer: dropdownRenderer, editable: (params) => { - const data = params?.data as { dataType: string } - return canEdit(data.dataType, 'units', null) + const data = params?.data as { to_data_type: string } + return canEdit(data.to_data_type, 'from_units', null) }, cellClass: (params) => { - const data = params?.data as { dataType: string } - return canEdit(data.dataType, 'units', null) ? canEditClass : '' + const data = params?.data as { to_data_type: string } + return canEdit(data.to_data_type, 'from_units', null) ? canEditClass : '' }, }, - { field: 'file_header', headerName: 'Data File Header' }, + { field: 'from_field', headerName: 'Data File Header' }, { field: 'row1', headerName: 'Row 1' }, { field: 'row2', headerName: 'Row 2' }, { field: 'row3', headerName: 'Row 3' }, diff --git a/src/app/modules/datasets/data-mappings/constants.ts b/src/app/modules/datasets/data-mappings/constants.ts index 51dded44..00504032 100644 --- a/src/app/modules/datasets/data-mappings/constants.ts +++ b/src/app/modules/datasets/data-mappings/constants.ts @@ -15,6 +15,11 @@ export const dataTypeMap: Record = { // wui: { display: 'Water Use Intensity', units: 'gal/ft²/year' }, } +const dataTypes = ['None', 'Number', 'Integer', 'Text', 'Datetime', 'Date', 'Boolean', 'Area', 'EUI', 'Geometry', 'GHG', 'GHG Intensity'] // 'Water Use', 'Water Use Intensity' +const displayDataTypes = [null, 'number', 'integer', 'string', 'datetime', 'date', 'boolean', 'area', 'eui', 'geometry', 'ghg', 'ghg_intensity'] // 'water_use', 'wui' + +export const displayToDataTypeMap: Record = Object.fromEntries(displayDataTypes.map((k, i) => [k, dataTypes[i]])) + export const unitMap: Record = { Area: ['ft²', 'm²'], EUI: [ @@ -52,6 +57,6 @@ export const dataTypeOptions = [ 'Geometry', 'GHG', 'GHG Intensity', - 'Water Use', - 'Water Use Intensity', + // 'Water Use', + // 'Water Use Intensity', ] diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.html b/src/app/modules/datasets/data-mappings/data-mapping.component.html index 6d41d6a8..14c6c70b 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.html +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.html @@ -48,9 +48,15 @@
-
-
-
Editable Cell
+
+
+
+
Editable Cell
+
+ + + +
fileId = this._router.snapshot.params.id as number @@ -72,6 +76,8 @@ export class DataMappingComponent implements OnDestroy, OnInit { gridOptions = gridOptions gridTheme$ = this._configService.gridTheme$ mappingSuggestions: MappingSuggestionsResponse + matchingPropertyColumns: string[] = [] + matchingTaxLotColumns: string[] = [] orgId: number rawColumnNames: string[] = [] rowData: Record[] = [] @@ -84,6 +90,7 @@ export class DataMappingComponent implements OnDestroy, OnInit { switchMap(() => this.getImportFile()), filter(Boolean), switchMap(() => this.getMappingData()), + switchMap(() => this.getMatchingColumns()), tap(() => { this.setGrid() }), ) .subscribe() @@ -118,14 +125,29 @@ export class DataMappingComponent implements OnDestroy, OnInit { ) } + getMatchingColumns() { + return forkJoin([ + this._organizationService.getMatchingCriteriaColumns(this.orgId, 'properties'), + this._organizationService.getMatchingCriteriaColumns(this.orgId, 'taxlots'), + ]) + .pipe( + take(1), + tap(([matchingPropertyColumns, matchingTaxLotColumns]) => { + this.matchingPropertyColumns = matchingPropertyColumns as string[] + this.matchingTaxLotColumns = matchingTaxLotColumns as string[] + }), + ) + + } + setGrid() { this.defaultRow = { isExtraData: false, omit: null, - seed_header: null, - inventory_type: this.defaultInventoryType, - dataType: null, - units: null, + to_field_display_name: null, + to_table_name: this.defaultInventoryType, + to_data_type: null, + from_units: null, } this.setColumnDefs() this.setRowData() @@ -149,7 +171,7 @@ export class DataMappingComponent implements OnDestroy, OnInit { const values = this.firstFiveRows.map((r) => r[header]) const rows: Record = Object.fromEntries(keys.map((k, i) => [k, values[i]])) - const data = { ...rows, ...this.defaultRow, file_header: header } + const data = { ...rows, ...this.defaultRow, from_field: header } this.rowData.push(data) } @@ -165,7 +187,7 @@ export class DataMappingComponent implements OnDestroy, OnInit { setAllInventoryType(value: InventoryDisplayType) { this.defaultInventoryType = value - this.gridApi.forEachNode((n) => n.setDataValue('inventory_type', value)) + this.gridApi.forEachNode((node) => node.setDataValue('to_table_display_name', value)) this.setColumns() } @@ -181,21 +203,24 @@ export class DataMappingComponent implements OnDestroy, OnInit { const column = this.columnMap[newValue] ?? null const dataTypeConfig = dataTypeMap[column?.data_type] ?? { display: 'None', units: null } - + const to_field = column?.column_name ?? newValue + console.log('set dataType', dataTypeConfig.display) node.setData({ ...node.data, isNewColumn: !column, isExtraData: column?.is_extra_data ?? true, - dataType: dataTypeConfig.display, - units: dataTypeConfig.units, + to_data_type: dataTypeConfig.display, + to_field, + from_units: dataTypeConfig.units, }) this.refreshNode(node) + this.validateData() } dataTypeChange = (params: CellValueChangedEvent): void => { const node = params.node as RowNode - node.setDataValue('units', null) + node.setDataValue('from_units', null) this.refreshNode(node) } @@ -211,14 +236,57 @@ export class DataMappingComponent implements OnDestroy, OnInit { const columns = this.defaultInventoryType === 'Tax Lot' ? taxlot_columns : property_columns const columnMap: Record = columns.reduce((acc, { column_name, display_name }) => ({ ...acc, [column_name]: display_name }), {}) - this.gridApi.forEachNode((node: RowNode<{ file_header: string }>) => { - const fileHeader = node.data.file_header + this.gridApi.forEachNode((node: RowNode<{ from_field: string }>) => { + const fileHeader = node.data.from_field const suggestedColumnName = suggested_column_mappings[fileHeader][1] const displayName = columnMap[suggestedColumnName] - node.setDataValue('seed_header', displayName) + node.setDataValue('to_field_display_name', displayName) }) } + // Format data for backend consumption + mapData() { + const result = [] + this.gridApi.forEachNode(({ data }: { data: DataMappingRow }) => { + if (data.omit) return // skip omitted rows + + const { from_field, from_units, to_data_type, to_field, to_field_display_name, to_table_name } = data + + result.push({ + from_field, + from_units: from_units?.replace('²', '**2') ?? null, + to_data_type: displayToDataTypeMap[to_data_type] ?? null, + to_field, + to_field_display_name, + to_table_name: to_table_name ?? this.defaultInventoryType, + }) + }) + console.log('Mapped Data:', result) + } + + validateData() { + const matchingColumns = this.defaultInventoryType === 'Tax Lot' ? this.matchingTaxLotColumns : this.matchingPropertyColumns + const toFields = [] + this.gridApi.forEachNode((node: RowNode) => { + if (node.data.omit) return // skip omitted rows + toFields.push(node.data.to_field) + }) + + // no duplicates + if (toFields.length !== new Set(toFields).size) { + this.dataValid = false + return + } + // at least one matching column + const hasMatchingCol = toFields.some((col) => matchingColumns.includes(col)) + if (!hasMatchingCol) { + this.dataValid = false + return + } + + this.dataValid = true + } + toggleHelp = () => { this.helpOpened = !this.helpOpened } From 966a8e27673869bf5f17db6661385e67d1ac30f5 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 2 Jul 2025 14:27:38 +0000 Subject: [PATCH 17/37] save mapping step --- src/@seed/api/dataset/dataset.types.ts | 10 + src/@seed/api/mapping/mapping.service.ts | 37 ++- .../data-mappings/data-mapping.component.html | 76 ++--- .../data-mappings/data-mapping.component.ts | 275 +++++++----------- .../data-mappings/{ => step1}/column-defs.ts | 0 .../data-mappings/{ => step1}/constants.ts | 4 +- .../step1/map-data.component.html | 42 +++ .../data-mappings/step1/map-data.component.ts | 245 ++++++++++++++++ .../step3/save-mappings.component.html | 29 ++ .../step3/save-mappings.component.ts | 97 ++++++ .../step4/match-marge.compponent.html | 3 + .../step4/match-margecompponent.ts | 30 ++ 12 files changed, 641 insertions(+), 207 deletions(-) rename src/app/modules/datasets/data-mappings/{ => step1}/column-defs.ts (100%) rename src/app/modules/datasets/data-mappings/{ => step1}/constants.ts (82%) create mode 100644 src/app/modules/datasets/data-mappings/step1/map-data.component.html create mode 100644 src/app/modules/datasets/data-mappings/step1/map-data.component.ts create mode 100644 src/app/modules/datasets/data-mappings/step3/save-mappings.component.html create mode 100644 src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts create mode 100644 src/app/modules/datasets/data-mappings/step4/match-marge.compponent.html create mode 100644 src/app/modules/datasets/data-mappings/step4/match-margecompponent.ts diff --git a/src/@seed/api/dataset/dataset.types.ts b/src/@seed/api/dataset/dataset.types.ts index cf1cf6ba..1134c480 100644 --- a/src/@seed/api/dataset/dataset.types.ts +++ b/src/@seed/api/dataset/dataset.types.ts @@ -64,3 +64,13 @@ export type DataMappingRow = { isExtraData?: boolean; // used internally, not part of the API isNewColumn?: boolean; // used internally, not part of the API } + +export type MappedData = { + mappings: DataMappingRow[]; +} + +export type MappingResultsResponse = { + status: string; + properties: Record[]; + tax_lots: Record[]; +} diff --git a/src/@seed/api/mapping/mapping.service.ts b/src/@seed/api/mapping/mapping.service.ts index ccfb7ac3..78de21fc 100644 --- a/src/@seed/api/mapping/mapping.service.ts +++ b/src/@seed/api/mapping/mapping.service.ts @@ -2,8 +2,10 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http' import { inject, Injectable } from '@angular/core' import { ErrorService } from '@seed/services' import { UserService } from '../user' -import { catchError, map, type Observable } from 'rxjs' +import { catchError, map, tap, type Observable } from 'rxjs' import type { FirstFiveRowsResponse, MappingSuggestionsResponse, RawColumnNamesResponse } from './mapping.types' +import { MappedData, MappingResultsResponse } from '../dataset' +import { ProgressResponse } from '../progress' @Injectable({ providedIn: 'root' }) export class MappingService { @@ -42,4 +44,37 @@ export class MappingService { }), ) } + + startMapping(orgId: number, importFileId: number, mappedData: MappedData): Observable { + const url = `api/v3/organizations/${orgId}/column_mappings/?import_file_id=${importFileId}` + return this._httpClient.post(url, mappedData) + .pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error starting mapping') + }), + ) + } + + remapBuildings(orgId: number, importFileId: number): Observable { + const url = `/api/v3/import_files/${importFileId}/map/?organization_id=${orgId}` + return this._httpClient.post(url, { remap: true, mark_as_done: false }) + .pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error remapping buildings') + }), + ) + } + + mappingResults(orgId: number, importFileId: number): Observable { + const url = `/api/v3/import_files/${importFileId}/mapping_results/?organization_id=${orgId}` + return this._httpClient.post(url, {}) + .pipe( + tap((response) => { + console.log(response) + }), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error fetching mapping results') + }), + ) + } } diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.html b/src/app/modules/datasets/data-mappings/data-mapping.component.html index 14c6c70b..e24d0927 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.html +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.html @@ -7,7 +7,7 @@ actionIcon: 'fa-solid:circle-question', }" > -
+
@@ -28,45 +28,47 @@ -
-
Cycle
-
{{ cycle?.name }}
-
-
-
Column Profile
-
none selected
-
- - -
- - - - Property Types - Tax Lot Types - -
- - -
-
-
-
Editable Cell
-
+ + + + + + + + - + + + + + + -
+ - - +
\ No newline at end of file diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.ts b/src/app/modules/datasets/data-mappings/data-mapping.component.ts index b63cff21..1cc33d99 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.ts +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ import { CommonModule } from '@angular/common' import type { OnDestroy, OnInit } from '@angular/core' -import { Component, inject } from '@angular/core' +import { Component, inject, ViewChild } from '@angular/core' import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { MatButtonModule } from '@angular/material/button' import { MatButtonToggleModule } from '@angular/material/button-toggle' @@ -9,89 +9,109 @@ import { MatDividerModule } from '@angular/material/divider' import { MatIconModule } from '@angular/material/icon' import { MatSelectModule } from '@angular/material/select' import { MatSidenavModule } from '@angular/material/sidenav' +import type { MatStepper} from '@angular/material/stepper' +import { MatStepperModule } from '@angular/material/stepper' import { ActivatedRoute } from '@angular/router' import { AgGridAngular } from 'ag-grid-angular' -import type { CellValueChangedEvent, ColDef, GridApi, GridReadyEvent, RowNode } from 'ag-grid-community' import { catchError, filter, forkJoin, of, Subject, switchMap, take, tap } from 'rxjs' -import { type Column, ColumnService } from '@seed/api/column' +import { ColumnService, type Column } from '@seed/api/column' import type { Cycle } from '@seed/api/cycle' import { CycleService } from '@seed/api/cycle/cycle.service' -import type { DataMappingRow, ImportFile } from '@seed/api/dataset' +import type { ImportFile, MappingResultsResponse } from '@seed/api/dataset' import { DatasetService } from '@seed/api/dataset' -import { InventoryService } from '@seed/api/inventory' import type { MappingSuggestionsResponse } from '@seed/api/mapping' import { MappingService } from '@seed/api/mapping' -import { OrganizationService } from '@seed/api/organization' +import { Organization, OrganizationService } from '@seed/api/organization' +import type { ProgressResponse } from '@seed/api/progress' import { UserService } from '@seed/api/user' -import { PageComponent } from '@seed/components' -import { ConfigService } from '@seed/services' -import type { InventoryDisplayType, Profile } from 'app/modules/inventory' -import { buildColumnDefs, gridOptions } from './column-defs' -import { dataTypeMap, displayToDataTypeMap } from './constants' +import { PageComponent, ProgressBarComponent } from '@seed/components' +import type { ProgressBarObj } from '@seed/services/uploader' +import { UploaderService } from '@seed/services/uploader' +import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' +import type { Profile } from 'app/modules/inventory' import { HelpComponent } from './help.component' +import { MapDataComponent } from './step1/map-data.component' +import { SaveMappingsComponent } from './step3/save-mappings.component' +import { MatchMergeProgressComponent } from './step4/match-margecompponent' @Component({ - selector: 'seed-data-mapping', + selector: 'seed-data-mapping-stepper', templateUrl: './data-mapping.component.html', imports: [ AgGridAngular, CommonModule, + FormsModule, HelpComponent, + MapDataComponent, + MatchMergeProgressComponent, MatButtonModule, MatButtonToggleModule, MatDividerModule, MatIconModule, MatSidenavModule, MatSelectModule, + MatStepperModule, PageComponent, + ProgressBarComponent, ReactiveFormsModule, - FormsModule, + SaveMappingsComponent, ], }) export class DataMappingComponent implements OnDestroy, OnInit { + @ViewChild('stepper') stepper!: MatStepper + @ViewChild(MapDataComponent) mapDataComponent!: MapDataComponent private readonly _unsubscribeAll$ = new Subject() - private _configService = inject(ConfigService) - private _cycleService = inject(CycleService) private _columnService = inject(ColumnService) + private _cycleService = inject(CycleService) private _datasetService = inject(DatasetService) - private _inventoryService = inject(InventoryService) private _mappingService = inject(MappingService) private _organizationService = inject(OrganizationService) + private _snackBar = inject(SnackBarService) private _router = inject(ActivatedRoute) + private _uploaderService = inject(UploaderService) private _userService = inject(UserService) columns: Column[] columnNames: string[] - columnMap: Record - columnDefs: ColDef[] + completed = { 1: false, 2: false, 3: false, 4: false } currentProfile: Profile cycle: Cycle - dataValid = false - defaultInventoryType: InventoryDisplayType = 'Property' - defaultRow: Record fileId = this._router.snapshot.params.id as number firstFiveRows: Record[] helpOpened = false importFile: ImportFile - gridApi: GridApi - gridOptions = gridOptions - gridTheme$ = this._configService.gridTheme$ + mappingResultsResponse: MappingResultsResponse mappingSuggestions: MappingSuggestionsResponse matchingPropertyColumns: string[] = [] matchingTaxLotColumns: string[] = [] + org: Organization orgId: number + propertyColumns: Column[] rawColumnNames: string[] = [] - rowData: Record[] = [] + taxlotColumns: Column[] + + progressBarObj: ProgressBarObj = { + message: [], + progress: 0, + total: 100, + complete: false, + statusMessage: '', + progressLastUpdated: null, + progressLastChecked: null, + } ngOnInit(): void { - this._userService.currentOrganizationId$ + // this._userService.currentOrganizationId$ + this._organizationService.currentOrganization$ .pipe( take(1), - tap((orgId) => this.orgId = orgId), + tap((org) => { + this.orgId = org.id + this.org = org + }), switchMap(() => this.getImportFile()), filter(Boolean), switchMap(() => this.getMappingData()), - switchMap(() => this.getMatchingColumns()), - tap(() => { this.setGrid() }), + switchMap(() => this.getColumns()), ) .subscribe() } @@ -106,6 +126,7 @@ export class DataMappingComponent implements OnDestroy, OnInit { }), ) } + getMappingData() { return forkJoin([ this._cycleService.getCycle(this.orgId, this.importFile.cycle), @@ -120,171 +141,91 @@ export class DataMappingComponent implements OnDestroy, OnInit { this.firstFiveRows = firstFiveRows this.mappingSuggestions = mappingSuggestions this.rawColumnNames = rawColumnNames - this.setColumns() + // this.setColumns() }), ) } - getMatchingColumns() { + getColumns() { return forkJoin([ this._organizationService.getMatchingCriteriaColumns(this.orgId, 'properties'), this._organizationService.getMatchingCriteriaColumns(this.orgId, 'taxlots'), + this._columnService.propertyColumns$.pipe(take(1)), + this._columnService.taxLotColumns$.pipe(take(1)), ]) .pipe( take(1), - tap(([matchingPropertyColumns, matchingTaxLotColumns]) => { + tap(([ + matchingPropertyColumns, + matchingTaxLotColumns, + propertyColumns, + taxlotColumns, + ]) => { this.matchingPropertyColumns = matchingPropertyColumns as string[] this.matchingTaxLotColumns = matchingTaxLotColumns as string[] + this.propertyColumns = propertyColumns + this.taxlotColumns = taxlotColumns }), ) - - } - - setGrid() { - this.defaultRow = { - isExtraData: false, - omit: null, - to_field_display_name: null, - to_table_name: this.defaultInventoryType, - to_data_type: null, - from_units: null, - } - this.setColumnDefs() - this.setRowData() } - setColumnDefs() { - this.columnDefs = buildColumnDefs( - this.columnNames, - this.importFile.uploaded_filename, - this.seedHeaderChange.bind(this), - this.dataTypeChange.bind(this), - ) + onCompleted(step: number) { + this.completed[step] = true + this.stepper.next() } - setRowData() { - this.rowData = [] - - // transpose first 5 rows to fit into the grid - for (const header of this.rawColumnNames) { - const keys = ['row1', 'row2', 'row3', 'row4', 'row5'] - const values = this.firstFiveRows.map((r) => r[header]) - const rows: Record = Object.fromEntries(keys.map((k, i) => [k, values[i]])) + startMapping() { + const mappedData = this.mapDataComponent.mappedData + this.columns = this.mapDataComponent.defaultInventoryType === 'Tax Lot' ? this.taxlotColumns : this.propertyColumns + this.nextStep(1) - const data = { ...rows, ...this.defaultRow, from_field: header } - this.rowData.push(data) + const failureFn = () => { + this._snackBar.alert('Error starting mapping') } - - for (const row of this.rowData) { - row.omit = false + const successFn = () => { + this.nextStep(2) + this.getMappingResults() } - } - - onGridReady(agGrid: GridReadyEvent) { - this.gridApi = agGrid.api - // this.gridApi.addEventListener('cellClicked', this.onCellClicked.bind(this) as (event: CellClickedEvent) => void) - } - - setAllInventoryType(value: InventoryDisplayType) { - this.defaultInventoryType = value - this.gridApi.forEachNode((node) => node.setDataValue('to_table_display_name', value)) - this.setColumns() - } - - setColumns() { - this.columns = this.defaultInventoryType === 'Tax Lot' ? this.mappingSuggestions?.taxlot_columns : this.mappingSuggestions?.property_columns - this.columnNames = this.columns.map((c) => c.display_name) - this.columnMap = this.columns.reduce((acc, curr) => ({ ...acc, [curr.display_name]: curr }), {}) - } - - seedHeaderChange = (params: CellValueChangedEvent): void => { - const node = params.node as RowNode - const newValue = params.newValue as string - const column = this.columnMap[newValue] ?? null - - const dataTypeConfig = dataTypeMap[column?.data_type] ?? { display: 'None', units: null } - const to_field = column?.column_name ?? newValue - console.log('set dataType', dataTypeConfig.display) - node.setData({ - ...node.data, - isNewColumn: !column, - isExtraData: column?.is_extra_data ?? true, - to_data_type: dataTypeConfig.display, - to_field, - from_units: dataTypeConfig.units, - }) - - this.refreshNode(node) - this.validateData() - } - dataTypeChange = (params: CellValueChangedEvent): void => { - const node = params.node as RowNode - node.setDataValue('from_units', null) - this.refreshNode(node) - } - - refreshNode(node: RowNode) { - this.gridApi.refreshCells({ - rowNodes: [node], - force: true, - }) - } - - copyHeadersToSeed() { - const { property_columns, taxlot_columns, suggested_column_mappings } = this.mappingSuggestions - const columns = this.defaultInventoryType === 'Tax Lot' ? taxlot_columns : property_columns - const columnMap: Record = columns.reduce((acc, { column_name, display_name }) => ({ ...acc, [column_name]: display_name }), {}) - - this.gridApi.forEachNode((node: RowNode<{ from_field: string }>) => { - const fileHeader = node.data.from_field - const suggestedColumnName = suggested_column_mappings[fileHeader][1] - const displayName = columnMap[suggestedColumnName] - node.setDataValue('to_field_display_name', displayName) - }) + this._mappingService.startMapping(this.orgId, this.fileId, mappedData) + .pipe( + take(1), + switchMap(() => this._mappingService.remapBuildings(this.orgId, this.fileId)), + tap((response: ProgressResponse) => { + this.progressBarObj.progress = response.progress + }), + switchMap((data) => { + return this._uploaderService.checkProgressLoop({ + progressKey: data.progress_key, + offset: 0, + multiplier: 1, + successFn, + failureFn, + progressBarObj: this.progressBarObj, + }) + }), + catchError((error) => { + console.log('Error starting mapping:', error) + return of(null) + }), + ) + .subscribe() } - // Format data for backend consumption - mapData() { - const result = [] - this.gridApi.forEachNode(({ data }: { data: DataMappingRow }) => { - if (data.omit) return // skip omitted rows - - const { from_field, from_units, to_data_type, to_field, to_field_display_name, to_table_name } = data - - result.push({ - from_field, - from_units: from_units?.replace('²', '**2') ?? null, - to_data_type: displayToDataTypeMap[to_data_type] ?? null, - to_field, - to_field_display_name, - to_table_name: to_table_name ?? this.defaultInventoryType, - }) - }) - console.log('Mapped Data:', result) + getMappingResults(): void { + this.nextStep(2) + this._mappingService.mappingResults(this.orgId, this.fileId) + .pipe( + tap((mappingResultsResponse) => { this.mappingResultsResponse = mappingResultsResponse }), + ) + .subscribe() } - validateData() { - const matchingColumns = this.defaultInventoryType === 'Tax Lot' ? this.matchingTaxLotColumns : this.matchingPropertyColumns - const toFields = [] - this.gridApi.forEachNode((node: RowNode) => { - if (node.data.omit) return // skip omitted rows - toFields.push(node.data.to_field) + nextStep(step: number) { + this.completed[step] = true + setTimeout(() => { + this.stepper.next() }) - - // no duplicates - if (toFields.length !== new Set(toFields).size) { - this.dataValid = false - return - } - // at least one matching column - const hasMatchingCol = toFields.some((col) => matchingColumns.includes(col)) - if (!hasMatchingCol) { - this.dataValid = false - return - } - - this.dataValid = true } toggleHelp = () => { diff --git a/src/app/modules/datasets/data-mappings/column-defs.ts b/src/app/modules/datasets/data-mappings/step1/column-defs.ts similarity index 100% rename from src/app/modules/datasets/data-mappings/column-defs.ts rename to src/app/modules/datasets/data-mappings/step1/column-defs.ts diff --git a/src/app/modules/datasets/data-mappings/constants.ts b/src/app/modules/datasets/data-mappings/step1/constants.ts similarity index 82% rename from src/app/modules/datasets/data-mappings/constants.ts rename to src/app/modules/datasets/data-mappings/step1/constants.ts index 00504032..b047d01c 100644 --- a/src/app/modules/datasets/data-mappings/constants.ts +++ b/src/app/modules/datasets/data-mappings/step1/constants.ts @@ -15,8 +15,8 @@ export const dataTypeMap: Record = { // wui: { display: 'Water Use Intensity', units: 'gal/ft²/year' }, } -const dataTypes = ['None', 'Number', 'Integer', 'Text', 'Datetime', 'Date', 'Boolean', 'Area', 'EUI', 'Geometry', 'GHG', 'GHG Intensity'] // 'Water Use', 'Water Use Intensity' -const displayDataTypes = [null, 'number', 'integer', 'string', 'datetime', 'date', 'boolean', 'area', 'eui', 'geometry', 'ghg', 'ghg_intensity'] // 'water_use', 'wui' +const displayDataTypes = ['None', 'Number', 'Integer', 'Text', 'Datetime', 'Date', 'Boolean', 'Area', 'EUI', 'Geometry', 'GHG', 'GHG Intensity'] // 'Water Use', 'Water Use Intensity' +const dataTypes = ['None', 'number', 'integer', 'string', 'datetime', 'date', 'boolean', 'area', 'eui', 'geometry', 'ghg', 'ghg_intensity'] // 'water_use', 'wui' export const displayToDataTypeMap: Record = Object.fromEntries(displayDataTypes.map((k, i) => [k, dataTypes[i]])) diff --git a/src/app/modules/datasets/data-mappings/step1/map-data.component.html b/src/app/modules/datasets/data-mappings/step1/map-data.component.html new file mode 100644 index 00000000..7e3ae3ec --- /dev/null +++ b/src/app/modules/datasets/data-mappings/step1/map-data.component.html @@ -0,0 +1,42 @@ +
+
Cycle
+
{{ cycle?.name }}
+
+
+
Column Profile
+
none selected
+
+ + +
+ + + + Property Types + Tax Lot Types + +
+ + +
+
+
+
Editable Cell
+
+ + + + +
+ + + \ No newline at end of file diff --git a/src/app/modules/datasets/data-mappings/step1/map-data.component.ts b/src/app/modules/datasets/data-mappings/step1/map-data.component.ts new file mode 100644 index 00000000..cfbce714 --- /dev/null +++ b/src/app/modules/datasets/data-mappings/step1/map-data.component.ts @@ -0,0 +1,245 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { CommonModule } from '@angular/common' +import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core' +import { Component, EventEmitter, inject, Input, Output } from '@angular/core' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { MatButtonModule } from '@angular/material/button' +import { MatButtonToggleModule } from '@angular/material/button-toggle' +import { MatDividerModule } from '@angular/material/divider' +import { MatIconModule } from '@angular/material/icon' +import { MatSelectModule } from '@angular/material/select' +import { MatSidenavModule } from '@angular/material/sidenav' +import { MatStepperModule } from '@angular/material/stepper' +import { ActivatedRoute } from '@angular/router' +import { type Column } from '@seed/api/column' +import type { Cycle } from '@seed/api/cycle' +import type { DataMappingRow, ImportFile } from '@seed/api/dataset' +import type { MappingSuggestionsResponse } from '@seed/api/mapping' +import { PageComponent } from '@seed/components' +import { ConfigService } from '@seed/services' +import type { ProgressBarObj } from '@seed/services/uploader' +import { AgGridAngular } from 'ag-grid-angular' +import type { CellValueChangedEvent, ColDef, GridApi, GridReadyEvent, RowNode } from 'ag-grid-community' +import type { InventoryDisplayType, Profile } from 'app/modules/inventory' +import { Subject } from 'rxjs' +import { HelpComponent } from '../help.component' +import { buildColumnDefs, gridOptions } from './column-defs' +import { dataTypeMap, displayToDataTypeMap } from './constants' + +@Component({ + selector: 'seed-map-data', + templateUrl: './map-data.component.html', + imports: [ + AgGridAngular, + CommonModule, + HelpComponent, + MatButtonModule, + MatButtonToggleModule, + MatDividerModule, + MatIconModule, + MatSidenavModule, + MatSelectModule, + MatStepperModule, + PageComponent, + ReactiveFormsModule, + FormsModule, + ], +}) +export class MapDataComponent implements OnChanges, OnDestroy { + @Input() orgId: number + @Input() importFile: ImportFile + @Input() cycle: Cycle + @Input() firstFiveRows: Record[] + @Input() mappingSuggestions: MappingSuggestionsResponse + @Input() rawColumnNames: string[] + @Input() matchingPropertyColumns: string[] + @Input() matchingTaxLotColumns: string[] + @Output() completed = new EventEmitter() + + private readonly _unsubscribeAll$ = new Subject() + private _configService = inject(ConfigService) + private _router = inject(ActivatedRoute) + columns: Column[] + columnNames: string[] + columnMap: Record + columnDefs: ColDef[] + currentProfile: Profile + dataValid = false + defaultInventoryType: InventoryDisplayType = 'Property' + defaultRow: Record + fileId = this._router.snapshot.params.id as number + gridApi: GridApi + gridOptions = gridOptions + gridTheme$ = this._configService.gridTheme$ + mappedData: { mappings: DataMappingRow[] } = { mappings: [] } + rowData: Record[] = [] + + progressBarObj: ProgressBarObj = { + message: [], + progress: 0, + total: 100, + complete: false, + statusMessage: '', + progressLastUpdated: null, + progressLastChecked: null, + } + + ngOnChanges(changes: SimpleChanges): void { + // property columns is the last value to be set + if ((changes.matchingPropertyColumns?.currentValue as string[])?.length) { + this.setColumns() + this.setGrid() + } + } + + setGrid() { + this.defaultRow = { + isExtraData: false, + omit: null, + to_field_display_name: null, + to_table_name: this.defaultInventoryType, + to_data_type: null, + from_units: null, + } + this.setColumnDefs() + this.setRowData() + } + + setColumnDefs() { + this.columnDefs = buildColumnDefs( + this.columnNames, + this.importFile.uploaded_filename, + this.seedHeaderChange.bind(this), + this.dataTypeChange.bind(this), + ) + } + + setRowData() { + this.rowData = [] + + // transpose first 5 rows to fit into the grid + for (const header of this.rawColumnNames) { + const keys = ['row1', 'row2', 'row3', 'row4', 'row5'] + const values = this.firstFiveRows.map((r) => r[header]) + const rows: Record = Object.fromEntries(keys.map((k, i) => [k, values[i]])) + + const data = { ...rows, ...this.defaultRow, from_field: header } + this.rowData.push(data) + } + + for (const row of this.rowData) { + row.omit = false + } + } + + onGridReady(agGrid: GridReadyEvent) { + this.gridApi = agGrid.api + } + + setAllInventoryType(value: InventoryDisplayType) { + this.defaultInventoryType = value + this.gridApi.forEachNode((node) => node.setDataValue('to_table_display_name', value)) + this.setColumns() + } + + setColumns() { + this.columns = this.defaultInventoryType === 'Tax Lot' ? this.mappingSuggestions?.taxlot_columns : this.mappingSuggestions?.property_columns + this.columnNames = this.columns.map((c) => c.display_name) + this.columnMap = Object.fromEntries(this.columns.map((c) => [c.display_name, c])) + } + + seedHeaderChange = (params: CellValueChangedEvent): void => { + const node = params.node as RowNode + const newValue = params.newValue as string + const column = this.columnMap[newValue] ?? null + + const dataTypeConfig = dataTypeMap[column?.data_type] ?? { display: 'None', units: null } + const to_field = column?.column_name ?? newValue + node.setData({ + ...node.data, + isNewColumn: !column, + isExtraData: column?.is_extra_data ?? true, + to_data_type: dataTypeConfig.display, + to_field, + from_units: dataTypeConfig.units, + }) + + this.refreshNode(node) + this.validateData() + } + + dataTypeChange = (params: CellValueChangedEvent): void => { + const node = params.node as RowNode + node.setDataValue('from_units', null) + this.refreshNode(node) + } + + refreshNode(node: RowNode) { + this.gridApi.refreshCells({ + rowNodes: [node], + force: true, + }) + } + + copyHeadersToSeed() { + const { property_columns, taxlot_columns, suggested_column_mappings } = this.mappingSuggestions + const columns = this.defaultInventoryType === 'Tax Lot' ? taxlot_columns : property_columns + const columnMap: Record = columns.reduce((acc, { column_name, display_name }) => ({ ...acc, [column_name]: display_name }), {}) + + this.gridApi.forEachNode((node: RowNode<{ from_field: string }>) => { + const fileHeader = node.data.from_field + const suggestedColumnName = suggested_column_mappings[fileHeader][1] + const displayName = columnMap[suggestedColumnName] + node.setDataValue('to_field_display_name', displayName) + }) + } + + // Format data for backend consumption + mapData() { + if (!this.dataValid) return + + this.gridApi.forEachNode(({ data }: { data: DataMappingRow }) => { + if (data.omit) return // skip omitted rows + + const { from_field, from_units, to_data_type, to_field, to_field_display_name, to_table_name } = data + + this.mappedData.mappings.push({ + from_field, + from_units: from_units?.replace('²', '**2') ?? null, + to_data_type: displayToDataTypeMap[to_data_type] ?? null, + to_field, + to_field_display_name, + to_table_name: to_table_name === 'Tax Lot' ? 'TaxLotState' : 'PropertyState', + }) + }) + this.completed.emit() + } + + validateData() { + const matchingColumns = this.defaultInventoryType === 'Tax Lot' ? this.matchingTaxLotColumns : this.matchingPropertyColumns + const toFields = [] + this.gridApi.forEachNode((node: RowNode) => { + if (node.data.omit) return // skip omitted rows + toFields.push(node.data.to_field) + }) + + // no duplicates + if (toFields.length !== new Set(toFields).size) { + this.dataValid = false + return + } + // at least one matching column + const hasMatchingCol = toFields.some((col) => matchingColumns.includes(col)) + if (!hasMatchingCol) { + this.dataValid = false + return + } + + this.dataValid = true + } + + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } +} diff --git a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html new file mode 100644 index 00000000..6f412d9b --- /dev/null +++ b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html @@ -0,0 +1,29 @@ +
+
Cycle
+
{{ cycle?.name }}
+
+ + + + +
+
+
+
Access Level Info
+
+ + +
+ +@if (rowData.length) { + + +} \ No newline at end of file diff --git a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts new file mode 100644 index 00000000..4d50ec7a --- /dev/null +++ b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts @@ -0,0 +1,97 @@ +import { CommonModule } from '@angular/common' +import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; +import { Component, EventEmitter, inject, Input, Output } from '@angular/core' +import { MatButtonModule } from '@angular/material/button' +import { MatDividerModule } from '@angular/material/divider' +import type { Column } from '@seed/api/column' +import type { Cycle } from '@seed/api/cycle' +import type { MappingResultsResponse } from '@seed/api/dataset' +import type { Organization } from '@seed/api/organization' +import { ConfigService } from '@seed/services' +import { AgGridAngular } from 'ag-grid-angular' +import type { ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' +import { Subject } from 'rxjs' + +@Component({ + selector: 'seed-save-mappings', + templateUrl: './save-mappings.component.html', + imports: [ + AgGridAngular, + CommonModule, + MatDividerModule, + MatButtonModule, + ], +}) +export class SaveMappingsComponent implements OnChanges, OnDestroy { + @Input() columns: Column[] + @Input() cycle: Cycle + @Input() mappingResultsResponse: MappingResultsResponse + @Input() org: Organization + @Input() orgId: number + @Output() completed = new EventEmitter() + + private _configService = inject(ConfigService) + private _unsubscribeAll$ = new Subject() + columnDefs: ColDef[] = [] + rowData: Record[] = [] + gridApi: GridApi + gridTheme$ = this._configService.gridTheme$ + mappingResults: Record[] = [] + + ngOnChanges(changes: SimpleChanges): void { + if (!changes.mappingResultsResponse?.currentValue) return + + const { properties, tax_lots } = this.mappingResultsResponse + this.mappingResults = tax_lots.length ? tax_lots : properties || [] + this.setGrid() + } + + setGrid() { + this.setColumnDefs() + this.setRowData() + } + + setColumnDefs() { + const aliClass = 'bg-primary bg-opacity-25' + + let keys = Object.keys(this.mappingResults[0] ?? {}) + // remove ALI & hidden cols + const excludeKeys = ['id', 'lot_number', 'raw_access_level_instance_error', ...this.org.access_level_names] + keys = keys.filter((k) => !excludeKeys.includes(k)) + + const hiddenColumnDefs = [ + { field: 'id', hide: true }, + { field: 'lot_number', hide: true }, + ] + + // ALI columns + const aliErrorDef = { field: 'raw_access_level_instance_error', headerName: 'Access Level Error', cellClass: aliClass } + let aliColumnDefs = this.org.access_level_names.map((name) => ({ field: name, cellClass: aliClass })) + aliColumnDefs = [aliErrorDef, ...aliColumnDefs] + + // Inventory Columns + const columnNameMap: Record = this.columns.reduce((acc, { name, display_name }) => ({ ...acc, [name]: display_name }), {}) + const inventoryColumnDefs = keys.map((key) => ({ field: key, headerName: columnNameMap[key] || key })) + + this.columnDefs = [...hiddenColumnDefs, ...aliColumnDefs, ...inventoryColumnDefs] + } + + setRowData() { + this.rowData = this.mappingResults + } + + onGridReady(agGrid: GridReadyEvent) { + this.gridApi = agGrid.api + } + + saveData() { + console.log('Saving data...') + console.log(this.mappingResults) + this.completed.emit() + } + + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } +} diff --git a/src/app/modules/datasets/data-mappings/step4/match-marge.compponent.html b/src/app/modules/datasets/data-mappings/step4/match-marge.compponent.html new file mode 100644 index 00000000..86cbcb42 --- /dev/null +++ b/src/app/modules/datasets/data-mappings/step4/match-marge.compponent.html @@ -0,0 +1,3 @@ +
+ STEP 4! +
\ No newline at end of file diff --git a/src/app/modules/datasets/data-mappings/step4/match-margecompponent.ts b/src/app/modules/datasets/data-mappings/step4/match-margecompponent.ts new file mode 100644 index 00000000..936a6913 --- /dev/null +++ b/src/app/modules/datasets/data-mappings/step4/match-margecompponent.ts @@ -0,0 +1,30 @@ +import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core' +import { Component } from '@angular/core' +import { ProgressBarComponent } from '@seed/components' +import { Subject } from 'rxjs' + +@Component({ + selector: 'seed-match-merge', + templateUrl: './match-marge.component.html', + imports: [ + ProgressBarComponent, + ], +}) +export class MatchMergeComponent implements OnChanges, OnDestroy { + private readonly _unsubscribeAll$ = new Subject() + + ngOnChanges(changes: SimpleChanges): void { + if (changes) { // what changes? + // do something + } + } + + startMatchMerge() { + console.log('start match merge') + } + + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } +} From b3517a1c61c1d179bad40c6d45a10b32a4a1b288 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 2 Jul 2025 15:52:08 +0000 Subject: [PATCH 18/37] uploaded data, step4 issues --- .../api/data-quality/data-quality.service.ts | 20 ++++++ src/@seed/api/mapping/mapping.service.ts | 25 +++++++- src/@seed/api/progress/progress.types.ts | 10 +++ .../services/uploader/uploader.service.ts | 13 ++++ .../data-mappings/data-mapping.component.html | 13 ++-- .../data-mappings/data-mapping.component.ts | 24 ++++--- .../step3/save-mappings.component.html | 5 +- .../step3/save-mappings.component.ts | 43 ++++++++++++- .../step4/match-marge.compponent.html | 3 - .../step4/match-margecompponent.ts | 30 --------- .../step4/match-merge.component.html | 7 +++ .../step4/match-merge.component.ts | 63 +++++++++++++++++++ 12 files changed, 201 insertions(+), 55 deletions(-) delete mode 100644 src/app/modules/datasets/data-mappings/step4/match-marge.compponent.html delete mode 100644 src/app/modules/datasets/data-mappings/step4/match-margecompponent.ts create mode 100644 src/app/modules/datasets/data-mappings/step4/match-merge.component.html create mode 100644 src/app/modules/datasets/data-mappings/step4/match-merge.component.ts diff --git a/src/@seed/api/data-quality/data-quality.service.ts b/src/@seed/api/data-quality/data-quality.service.ts index 7fa0281d..9202dc71 100644 --- a/src/@seed/api/data-quality/data-quality.service.ts +++ b/src/@seed/api/data-quality/data-quality.service.ts @@ -5,6 +5,7 @@ import { OrganizationService } from '@seed/api/organization' import { ErrorService } from '@seed/services' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' import type { Rule } from './data-quality.types' +import { DQCProgressResponse } from '../progress' @Injectable({ providedIn: 'root' }) export class DataQualityService { @@ -79,4 +80,23 @@ export class DataQualityService { }), ) } + + startDataQualityCheckForImportFile(orgId: number, importFileId: number): Observable { + const url = `/api/v3/import_files/${importFileId}/start_data_quality_checks/?organization_id=${orgId}` + return this._httpClient.post(url, {}) + .pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error starting data quality checks for import file') + }), + ) + } + + getDataQualityResults(orgId: number, runId: number): Observable { + const url = `/api/v3/data_quality_checks/results/?organization_id=${orgId}&run_id=${runId}` + return this._httpClient.get(url).pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error fetching data quality results') + }), + ) + } } diff --git a/src/@seed/api/mapping/mapping.service.ts b/src/@seed/api/mapping/mapping.service.ts index 78de21fc..5d0b725f 100644 --- a/src/@seed/api/mapping/mapping.service.ts +++ b/src/@seed/api/mapping/mapping.service.ts @@ -5,7 +5,7 @@ import { UserService } from '../user' import { catchError, map, tap, type Observable } from 'rxjs' import type { FirstFiveRowsResponse, MappingSuggestionsResponse, RawColumnNamesResponse } from './mapping.types' import { MappedData, MappingResultsResponse } from '../dataset' -import { ProgressResponse } from '../progress' +import { ProgressResponse, SubProgressResponse } from '../progress' @Injectable({ providedIn: 'root' }) export class MappingService { @@ -77,4 +77,27 @@ export class MappingService { }), ) } + + mappingDone(orgId: number, importFileId: number): Observable<{ message: string; status: string }> { + const url = `/api/v3/import_files/${importFileId}/mapping_done/?organization_id=${orgId}` + return this._httpClient.post<{ message: string; status: string }>(url, {}) + .pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error fetching mapping results') + }), + ) + } + + startMatchMerge(orgId: number, importFileId: number): Observable { + const url = `/api/v3/import_files/${importFileId}/start_system_matching_and_geocoding/?organization_id=${orgId}` + return this._httpClient.post(url, {}) + .pipe( + tap((response) => { + console.log('Match merge started:', response) + }), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error starting match merge') + }), + ) + } } diff --git a/src/@seed/api/progress/progress.types.ts b/src/@seed/api/progress/progress.types.ts index 5b90543e..865af90b 100644 --- a/src/@seed/api/progress/progress.types.ts +++ b/src/@seed/api/progress/progress.types.ts @@ -14,3 +14,13 @@ export type ProgressResponse = { total_records?: number; completed_records?: number; } + +export type DQCProgressResponse = { + progress: ProgressResponse; + progress_key: string; +} + +export type SubProgressResponse = { + progress_data: ProgressResponse; + sub_progress_data: ProgressResponse; +} diff --git a/src/@seed/services/uploader/uploader.service.ts b/src/@seed/services/uploader/uploader.service.ts index 34b807ee..c676e608 100644 --- a/src/@seed/services/uploader/uploader.service.ts +++ b/src/@seed/services/uploader/uploader.service.ts @@ -8,6 +8,7 @@ import { ErrorService } from '../error' import type { CheckProgressLoopParams, GreenButtonMeterPreview, + ProgressBarObj, SensorPreviewResponse, SensorReadingPreview, UpdateProgressBarObjParams, @@ -19,6 +20,18 @@ export class UploaderService { private _httpClient = inject(HttpClient) private _errorService = inject(ErrorService) + get defaultProgressBarObj(): ProgressBarObj { + return { + message: [], + progress: 0, + total: 100, + complete: false, + statusMessage: '', + progressLastUpdated: null, + progressLastChecked: null, + } + } + /* * Checks a progress key for updates until it completes */ diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.html b/src/app/modules/datasets/data-mappings/data-mapping.component.html index e24d0927..07bd686f 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.html +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.html @@ -47,7 +47,7 @@ @@ -55,16 +55,21 @@ - + abc + + xyz diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.ts b/src/app/modules/datasets/data-mappings/data-mapping.component.ts index 1cc33d99..aec6041a 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.ts +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.ts @@ -32,7 +32,7 @@ import type { Profile } from 'app/modules/inventory' import { HelpComponent } from './help.component' import { MapDataComponent } from './step1/map-data.component' import { SaveMappingsComponent } from './step3/save-mappings.component' -import { MatchMergeProgressComponent } from './step4/match-margecompponent' +import { MatchMergeComponent } from './step4/match-merge.component' @Component({ selector: 'seed-data-mapping-stepper', @@ -43,7 +43,7 @@ import { MatchMergeProgressComponent } from './step4/match-margecompponent' FormsModule, HelpComponent, MapDataComponent, - MatchMergeProgressComponent, + MatchMergeComponent, MatButtonModule, MatButtonToggleModule, MatDividerModule, @@ -60,6 +60,7 @@ import { MatchMergeProgressComponent } from './step4/match-margecompponent' export class DataMappingComponent implements OnDestroy, OnInit { @ViewChild('stepper') stepper!: MatStepper @ViewChild(MapDataComponent) mapDataComponent!: MapDataComponent + @ViewChild(MatchMergeComponent) matchMergeComponent!: MatchMergeComponent private readonly _unsubscribeAll$ = new Subject() private _columnService = inject(ColumnService) private _cycleService = inject(CycleService) @@ -89,15 +90,7 @@ export class DataMappingComponent implements OnDestroy, OnInit { rawColumnNames: string[] = [] taxlotColumns: Column[] - progressBarObj: ProgressBarObj = { - message: [], - progress: 0, - total: 100, - complete: false, - statusMessage: '', - progressLastUpdated: null, - progressLastChecked: null, - } + progressBarObj = this._uploaderService.defaultProgressBarObj ngOnInit(): void { // this._userService.currentOrganizationId$ @@ -221,8 +214,13 @@ export class DataMappingComponent implements OnDestroy, OnInit { .subscribe() } - nextStep(step: number) { - this.completed[step] = true + startMatchMerge() { + this.nextStep(3) + this.matchMergeComponent.startMatchMerge() + } + + nextStep(currentStep: number) { + this.completed[currentStep] = true setTimeout(() => { this.stepper.next() }) diff --git a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html index 6f412d9b..b1cb0941 100644 --- a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html +++ b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html @@ -12,7 +12,10 @@
Access Level Info
- +
+ + +
@if (rowData.length) { diff --git a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts index 4d50ec7a..6046029a 100644 --- a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts +++ b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts @@ -5,12 +5,14 @@ import { MatButtonModule } from '@angular/material/button' import { MatDividerModule } from '@angular/material/divider' import type { Column } from '@seed/api/column' import type { Cycle } from '@seed/api/cycle' -import type { MappingResultsResponse } from '@seed/api/dataset' +import { DataQualityService } from '@seed/api/data-quality'; +import type { ImportFile, MappingResultsResponse } from '@seed/api/dataset' import type { Organization } from '@seed/api/organization' import { ConfigService } from '@seed/services' +import { ProgressBarObj, UploaderService } from '@seed/services/uploader'; import { AgGridAngular } from 'ag-grid-angular' import type { ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' -import { Subject } from 'rxjs' +import { Subject, switchMap, take } from 'rxjs' @Component({ selector: 'seed-save-mappings', @@ -25,27 +27,58 @@ import { Subject } from 'rxjs' export class SaveMappingsComponent implements OnChanges, OnDestroy { @Input() columns: Column[] @Input() cycle: Cycle + @Input() importFile: ImportFile @Input() mappingResultsResponse: MappingResultsResponse @Input() org: Organization @Input() orgId: number @Output() completed = new EventEmitter() private _configService = inject(ConfigService) + private _dataQualityService = inject(DataQualityService) + private _uploaderService = inject(UploaderService) private _unsubscribeAll$ = new Subject() columnDefs: ColDef[] = [] rowData: Record[] = [] gridApi: GridApi gridTheme$ = this._configService.gridTheme$ mappingResults: Record[] = [] + dqcComplete = false + + progressBarObj = this._uploaderService.defaultProgressBarObj ngOnChanges(changes: SimpleChanges): void { if (!changes.mappingResultsResponse?.currentValue) return const { properties, tax_lots } = this.mappingResultsResponse this.mappingResults = tax_lots.length ? tax_lots : properties || [] + this.startDQC() this.setGrid() } + startDQC() { + const successFn = () => { + this.dqcComplete = true + } + // eslint-disable-next-line @typescript-eslint/no-empty-function + const failureFn = () => {} + + this._dataQualityService.startDataQualityCheckForImportFile(this.orgId, this.importFile.id) + .pipe( + take(1), + switchMap(({ progress_key }) => { + return this._uploaderService.checkProgressLoop({ + progressKey: progress_key, + offset: 0, + multiplier: 1, + successFn, + failureFn, + progressBarObj: this.progressBarObj, + }) + }), + ) + .subscribe() + } + setGrid() { this.setColumnDefs() this.setRowData() @@ -86,10 +119,14 @@ export class SaveMappingsComponent implements OnChanges, OnDestroy { saveData() { console.log('Saving data...') - console.log(this.mappingResults) + // console.log(this.mappingResults) this.completed.emit() } + showDataQualityResults() { + console.log('open modal showing dqc results') + } + ngOnDestroy(): void { this._unsubscribeAll$.next() this._unsubscribeAll$.complete() diff --git a/src/app/modules/datasets/data-mappings/step4/match-marge.compponent.html b/src/app/modules/datasets/data-mappings/step4/match-marge.compponent.html deleted file mode 100644 index 86cbcb42..00000000 --- a/src/app/modules/datasets/data-mappings/step4/match-marge.compponent.html +++ /dev/null @@ -1,3 +0,0 @@ -
- STEP 4! -
\ No newline at end of file diff --git a/src/app/modules/datasets/data-mappings/step4/match-margecompponent.ts b/src/app/modules/datasets/data-mappings/step4/match-margecompponent.ts deleted file mode 100644 index 936a6913..00000000 --- a/src/app/modules/datasets/data-mappings/step4/match-margecompponent.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core' -import { Component } from '@angular/core' -import { ProgressBarComponent } from '@seed/components' -import { Subject } from 'rxjs' - -@Component({ - selector: 'seed-match-merge', - templateUrl: './match-marge.component.html', - imports: [ - ProgressBarComponent, - ], -}) -export class MatchMergeComponent implements OnChanges, OnDestroy { - private readonly _unsubscribeAll$ = new Subject() - - ngOnChanges(changes: SimpleChanges): void { - if (changes) { // what changes? - // do something - } - } - - startMatchMerge() { - console.log('start match merge') - } - - ngOnDestroy(): void { - this._unsubscribeAll$.next() - this._unsubscribeAll$.complete() - } -} diff --git a/src/app/modules/datasets/data-mappings/step4/match-merge.component.html b/src/app/modules/datasets/data-mappings/step4/match-merge.component.html new file mode 100644 index 00000000..fa3a8031 --- /dev/null +++ b/src/app/modules/datasets/data-mappings/step4/match-merge.component.html @@ -0,0 +1,7 @@ +
+ +
\ No newline at end of file diff --git a/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts b/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts new file mode 100644 index 00000000..652055cc --- /dev/null +++ b/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts @@ -0,0 +1,63 @@ +import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core' +import { Component, inject, Input } from '@angular/core' +import { DataQualityService } from '@seed/api/data-quality' +import { MappingService } from '@seed/api/mapping' +import { SubProgressResponse } from '@seed/api/progress' +import { ProgressBarComponent } from '@seed/components' +import { ProgressBarObj, UploaderService } from '@seed/services/uploader' +import { EMPTY, Subject, switchMap, take } from 'rxjs' + +@Component({ + selector: 'seed-match-merge', + templateUrl: './match-merge.component.html', + imports: [ + ProgressBarComponent, + ], +}) +export class MatchMergeComponent implements OnDestroy { + @Input() importFileId: number + @Input() orgId: number + + private _mappingService = inject(MappingService) + private _uploaderService = inject(UploaderService) + private readonly _unsubscribeAll$ = new Subject() + + progressBarObj = this._uploaderService.defaultProgressBarObj + subProgressBarObj = this._uploaderService.defaultProgressBarObj + + startMatchMerge() { + this._mappingService.mappingDone(this.orgId, this.importFileId) + .pipe( + take(1), + switchMap(() => this._mappingService.startMatchMerge(this.orgId, this.importFileId)), + take(1), + switchMap((data) => this.checkProgress(data)), + ) + .subscribe() + } + + checkProgress(data: SubProgressResponse) { + const successFn = () => { + console.log('success') + } + const failureFn = () => { + console.log('failure') + } + + const { progress_data } = data + + return this._uploaderService.checkProgressLoop({ + progressKey: progress_data.progress_key, + offset: 0, + multiplier: 1, + successFn, + failureFn, + progressBarObj: this.progressBarObj, + }) + } + + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } +} From bd0eb0be217b6b5be738258d7991c3b645c32c9f Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 2 Jul 2025 16:24:37 +0000 Subject: [PATCH 19/37] link to ptl step4 --- .../data-mappings/data-mapping.component.html | 4 ++-- .../data-mappings/data-mapping.component.ts | 13 ++++++++---- .../step3/save-mappings.component.ts | 18 +++++++++++++---- .../step4/match-merge.component.html | 20 ++++++++++++++----- .../step4/match-merge.component.ts | 16 +++++++++++---- 5 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.html b/src/app/modules/datasets/data-mappings/data-mapping.component.html index 07bd686f..cb7092e9 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.html +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.html @@ -60,16 +60,16 @@ [org]="org" [orgId]="orgId" (completed)="startMatchMerge()" + (inventoryTypeChange)="onInventoryTypeChange($event)" > - abc - xyz diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.ts b/src/app/modules/datasets/data-mappings/data-mapping.component.ts index aec6041a..88378d5c 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.ts +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.ts @@ -14,21 +14,21 @@ import { MatStepperModule } from '@angular/material/stepper' import { ActivatedRoute } from '@angular/router' import { AgGridAngular } from 'ag-grid-angular' import { catchError, filter, forkJoin, of, Subject, switchMap, take, tap } from 'rxjs' -import { ColumnService, type Column } from '@seed/api/column' +import { type Column, ColumnService } from '@seed/api/column' import type { Cycle } from '@seed/api/cycle' import { CycleService } from '@seed/api/cycle/cycle.service' import type { ImportFile, MappingResultsResponse } from '@seed/api/dataset' import { DatasetService } from '@seed/api/dataset' import type { MappingSuggestionsResponse } from '@seed/api/mapping' import { MappingService } from '@seed/api/mapping' -import { Organization, OrganizationService } from '@seed/api/organization' +import type { Organization } from '@seed/api/organization'; +import { OrganizationService } from '@seed/api/organization' import type { ProgressResponse } from '@seed/api/progress' import { UserService } from '@seed/api/user' import { PageComponent, ProgressBarComponent } from '@seed/components' -import type { ProgressBarObj } from '@seed/services/uploader' import { UploaderService } from '@seed/services/uploader' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' -import type { Profile } from 'app/modules/inventory' +import type { InventoryType, Profile } from 'app/modules/inventory' import { HelpComponent } from './help.component' import { MapDataComponent } from './step1/map-data.component' import { SaveMappingsComponent } from './step3/save-mappings.component' @@ -80,6 +80,7 @@ export class DataMappingComponent implements OnDestroy, OnInit { firstFiveRows: Record[] helpOpened = false importFile: ImportFile + inventoryType: InventoryType = 'properties' mappingResultsResponse: MappingResultsResponse mappingSuggestions: MappingSuggestionsResponse matchingPropertyColumns: string[] = [] @@ -226,6 +227,10 @@ export class DataMappingComponent implements OnDestroy, OnInit { }) } + onInventoryTypeChange(inventoryType: InventoryType) { + this.inventoryType = inventoryType + } + toggleHelp = () => { this.helpOpened = !this.helpOpened } diff --git a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts index 6046029a..11fd2f00 100644 --- a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts +++ b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts @@ -3,16 +3,17 @@ import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, inject, Input, Output } from '@angular/core' import { MatButtonModule } from '@angular/material/button' import { MatDividerModule } from '@angular/material/divider' +import { AgGridAngular } from 'ag-grid-angular' +import { Subject, switchMap, take } from 'rxjs' import type { Column } from '@seed/api/column' import type { Cycle } from '@seed/api/cycle' import { DataQualityService } from '@seed/api/data-quality'; import type { ImportFile, MappingResultsResponse } from '@seed/api/dataset' import type { Organization } from '@seed/api/organization' import { ConfigService } from '@seed/services' -import { ProgressBarObj, UploaderService } from '@seed/services/uploader'; -import { AgGridAngular } from 'ag-grid-angular' +import { UploaderService } from '@seed/services/uploader' import type { ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' -import { Subject, switchMap, take } from 'rxjs' +import type { InventoryType } from 'app/modules/inventory' @Component({ selector: 'seed-save-mappings', @@ -32,6 +33,7 @@ export class SaveMappingsComponent implements OnChanges, OnDestroy { @Input() org: Organization @Input() orgId: number @Output() completed = new EventEmitter() + @Output() inventoryTypeChange = new EventEmitter() private _configService = inject(ConfigService) private _dataQualityService = inject(DataQualityService) @@ -43,6 +45,7 @@ export class SaveMappingsComponent implements OnChanges, OnDestroy { gridTheme$ = this._configService.gridTheme$ mappingResults: Record[] = [] dqcComplete = false + inventoryType: InventoryType progressBarObj = this._uploaderService.defaultProgressBarObj @@ -50,7 +53,14 @@ export class SaveMappingsComponent implements OnChanges, OnDestroy { if (!changes.mappingResultsResponse?.currentValue) return const { properties, tax_lots } = this.mappingResultsResponse - this.mappingResults = tax_lots.length ? tax_lots : properties || [] + if (tax_lots.length) { + this.mappingResults = tax_lots + this.inventoryTypeChange.emit('taxlots') + } else { + this.mappingResults = properties + this.inventoryTypeChange.emit('properties') + } + this.startDQC() this.setGrid() } diff --git a/src/app/modules/datasets/data-mappings/step4/match-merge.component.html b/src/app/modules/datasets/data-mappings/step4/match-merge.component.html index fa3a8031..efbf94d5 100644 --- a/src/app/modules/datasets/data-mappings/step4/match-merge.component.html +++ b/src/app/modules/datasets/data-mappings/step4/match-merge.component.html @@ -1,7 +1,17 @@
- + @if (inProgress) { + + } @else { + + }
\ No newline at end of file diff --git a/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts b/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts index 652055cc..e71257fb 100644 --- a/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts +++ b/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts @@ -1,26 +1,33 @@ -import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core' +import { CommonModule } from '@angular/common' +import type { OnDestroy } from '@angular/core' import { Component, inject, Input } from '@angular/core' -import { DataQualityService } from '@seed/api/data-quality' +import { MatButtonModule } from '@angular/material/button' +import { RouterModule } from '@angular/router' import { MappingService } from '@seed/api/mapping' import { SubProgressResponse } from '@seed/api/progress' import { ProgressBarComponent } from '@seed/components' -import { ProgressBarObj, UploaderService } from '@seed/services/uploader' -import { EMPTY, Subject, switchMap, take } from 'rxjs' +import { UploaderService } from '@seed/services/uploader' +import { Subject, switchMap, take } from 'rxjs' @Component({ selector: 'seed-match-merge', templateUrl: './match-merge.component.html', imports: [ + CommonModule, + MatButtonModule, ProgressBarComponent, + RouterModule, ], }) export class MatchMergeComponent implements OnDestroy { @Input() importFileId: number @Input() orgId: number + @Input() inventoryType private _mappingService = inject(MappingService) private _uploaderService = inject(UploaderService) private readonly _unsubscribeAll$ = new Subject() + inProgress = true progressBarObj = this._uploaderService.defaultProgressBarObj subProgressBarObj = this._uploaderService.defaultProgressBarObj @@ -39,6 +46,7 @@ export class MatchMergeComponent implements OnDestroy { checkProgress(data: SubProgressResponse) { const successFn = () => { console.log('success') + this.inProgress = false } const failureFn = () => { console.log('failure') From e0dae9a2d18960100b70384d5664d8d0895b3642 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Thu, 3 Jul 2025 16:00:40 +0000 Subject: [PATCH 20/37] subprogress fxnal --- .../progress/progress-bar.component.html | 37 ++++++++++++---- .../progress/progress-bar.component.ts | 35 +++++++++++---- .../services/uploader/uploader.service.ts | 43 +++++++++++++++--- src/@seed/services/uploader/uploader.types.ts | 1 + .../data-mappings/data-mapping.component.ts | 9 +++- .../step3/save-mappings.component.html | 25 ++++++++++- .../step3/save-mappings.component.ts | 8 +++- .../step4/match-merge.component.html | 4 ++ .../step4/match-merge.component.ts | 44 ++++++++++++------- 9 files changed, 164 insertions(+), 42 deletions(-) diff --git a/src/@seed/components/progress/progress-bar.component.html b/src/@seed/components/progress/progress-bar.component.html index 1a4c5ed5..d8aa727f 100644 --- a/src/@seed/components/progress/progress-bar.component.html +++ b/src/@seed/components/progress/progress-bar.component.html @@ -1,13 +1,34 @@
-
-
- -
{{ title }}
+
+
+
+ +
{{ title }}
+
+ @if (progress) { +
+ {{ progressString }} +
+ }
- @if (progressMode === 'determinate') { -
{{ progress | number: '1.0-0' }} / {{ total | number: '1.0-0' }}
- } + +
- + @if (showSubProgress && subProgress && subProgress < 100) { +
+
+
+
{{ subTitle }}
+
+ @if (subProgress) { +
+ {{ subProgressString }} +
+ } +
+ + +
+ }
diff --git a/src/@seed/components/progress/progress-bar.component.ts b/src/@seed/components/progress/progress-bar.component.ts index 50f8c4ce..2cf0b5af 100644 --- a/src/@seed/components/progress/progress-bar.component.ts +++ b/src/@seed/components/progress/progress-bar.component.ts @@ -1,31 +1,50 @@ import { CommonModule } from '@angular/common' import { Component, Input } from '@angular/core' +import { MatDividerModule } from '@angular/material/divider' import { MatIconModule } from '@angular/material/icon' -import type { ProgressBarMode } from '@angular/material/progress-bar' import { MatProgressBarModule } from '@angular/material/progress-bar' +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner' @Component({ selector: 'seed-progress-bar', templateUrl: './progress-bar.component.html', - imports: [CommonModule, MatProgressBarModule, MatIconModule], + imports: [CommonModule, MatDividerModule, MatProgressBarModule, MatIconModule, MatProgressSpinnerModule], }) export class ProgressBarComponent { @Input() total: number @Input() progress: number @Input() title: string @Input() outline = false + @Input() showSubProgress? = false + @Input() subProgress?: number + @Input() subTotal?: number + @Input() subTitle?: string get percent() { return (this.progress / this.total) * 100 } - get showNumericProgress() { - if (this.progressMode === 'indeterminate') return false - return this.progress && this.progress < this.total + get progressString() { + return this.getProgressString(this.total, this.progress) } - get progressMode() { - const mode = this.progress ? 'determinate' : 'indeterminate' - return mode as ProgressBarMode + get subProgressString() { + if (!this.showSubProgress) return + return this.getProgressString(this.subTotal, this.subProgress) + } + + get subPercent() { + return this.subTotal ? (this.subProgress / this.subTotal) * 100 : undefined + } + + getProgressMode(progress) { + return progress ? 'determinate' : 'indeterminate' + } + + getProgressString(totalFloat: number, progressFloat: number) { + const total = Math.round(totalFloat) + const progress = Math.round(progressFloat) + const suffix = total === 100 ? '%' : `/ ${total}` + return `${progress} ${suffix}` } } diff --git a/src/@seed/services/uploader/uploader.service.ts b/src/@seed/services/uploader/uploader.service.ts index c676e608..b1cb1aae 100644 --- a/src/@seed/services/uploader/uploader.service.ts +++ b/src/@seed/services/uploader/uploader.service.ts @@ -2,7 +2,7 @@ import type { HttpErrorResponse } from '@angular/common/http' import { HttpClient } from '@angular/common/http' import { inject, Injectable } from '@angular/core' import type { Observable } from 'rxjs' -import { catchError, interval, of, switchMap, takeWhile, tap, throwError } from 'rxjs' +import { catchError, combineLatest, filter, finalize, interval, map, of, repeat, startWith, Subject, switchMap, takeUntil, takeWhile, tap, throwError } from 'rxjs' import type { ProgressResponse } from '@seed/api/progress' import { ErrorService } from '../error' import type { @@ -42,16 +42,14 @@ export class UploaderService { successFn, failureFn, progressBarObj, + subProgress = false, }: CheckProgressLoopParams): Observable { const isCompleted = (status: string) => ['error', 'success', 'warning'].includes(status) - return interval(750).pipe( + let progressLoop$ = interval(750).pipe( switchMap(() => this.checkProgress(progressKey)), tap((response) => { this._updateProgressBarObj({ data: response, offset, multiplier, progressBarObj }) - }), - takeWhile((response) => !isCompleted(response.status), true), // end stream - tap((response) => { if (response.status === 'success') successFn() }), catchError(() => { @@ -60,6 +58,39 @@ export class UploaderService { return throwError(() => new Error('Progress check failed')) }), ) + + // subProgress loops run until parent progress completes + if (!subProgress) { + progressLoop$ = progressLoop$.pipe( + takeWhile((response) => !isCompleted(response.status), true), // end stream + ) + } + + return progressLoop$ + } + + /* + * Check the progress of Main Progress and its Sub Progress + * Main progress will run until it completes + * Sub Progresses can complete several times and will run continuously until Main Progress is completed + * the stop$ stream is used to end the Sub Progress stream + */ + checkProgressLoopMainSub(mainParams: CheckProgressLoopParams, subParams: CheckProgressLoopParams) { + const stop$ = new Subject() + const main$ = this.checkProgressLoop(mainParams) + .pipe( + finalize(() => { + stop$.next() + stop$.complete() + }), + ) + + const sub$ = this.checkProgressLoop({ ...subParams, subProgress: true }) + .pipe( + takeUntil(stop$), + ) + + return combineLatest([main$, sub$]) } /* @@ -75,6 +106,8 @@ export class UploaderService { ) } + + greenButtonMetersPreview(orgId: number, viewId: number, systemId: number, fileId: number): Observable { const url = `/api/v3/import_files/${fileId}/greenbutton_meters_preview/` const params: Record = { organization_id: orgId } diff --git a/src/@seed/services/uploader/uploader.types.ts b/src/@seed/services/uploader/uploader.types.ts index ac192eac..93dbf45a 100644 --- a/src/@seed/services/uploader/uploader.types.ts +++ b/src/@seed/services/uploader/uploader.types.ts @@ -20,6 +20,7 @@ export type CheckProgressLoopParams = { successFn: () => void; failureFn: () => void; progressBarObj: ProgressBarObj; + subProgress?: boolean; } export type UpdateProgressBarObjParams = { diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.ts b/src/app/modules/datasets/data-mappings/data-mapping.component.ts index ec269363..1f2f47c6 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.ts +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.ts @@ -28,7 +28,7 @@ import { UploaderService } from '@seed/services/uploader' import { AgGridAngular } from 'ag-grid-angular' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' import type { InventoryType, Profile } from 'app/modules/inventory' -import { catchError, filter, forkJoin, of, Subject, switchMap, take, tap } from 'rxjs' +import { catchError, filter, forkJoin, of, Subject, switchMap, take, takeUntil, tap } from 'rxjs' import { HelpComponent } from './help.component' import { MapDataComponent } from './step1/map-data.component' import { SaveMappingsComponent } from './step3/save-mappings.component' @@ -182,12 +182,16 @@ export class DataMappingComponent implements OnDestroy, OnInit { this._mappingService.startMapping(this.orgId, this.fileId, mappedData) .pipe( - take(1), switchMap(() => this._mappingService.remapBuildings(this.orgId, this.fileId)), tap((response: ProgressResponse) => { this.progressBarObj.progress = response.progress }), switchMap((data) => { + if (data.progress === 100) { + successFn() + return of(null) + } + return this._uploaderService.checkProgressLoop({ progressKey: data.progress_key, offset: 0, @@ -197,6 +201,7 @@ export class DataMappingComponent implements OnDestroy, OnInit { progressBarObj: this.progressBarObj, }) }), + takeUntil(this._unsubscribeAll$), catchError((error) => { console.log('Error starting mapping:', error) return of(null) diff --git a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html index b1cb0941..dcef41f0 100644 --- a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html +++ b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html @@ -13,8 +13,20 @@
- - + +
@@ -29,4 +41,13 @@ (gridReady)="onGridReady($event)" >
+} @else { +
+
+ +
Fetching Mapping Results...
+
+ +
+ } \ No newline at end of file diff --git a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts index 09f7bf38..65bb5c15 100644 --- a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts +++ b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts @@ -3,9 +3,11 @@ import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, inject, Input, Output } from '@angular/core' import { MatButtonModule } from '@angular/material/button' import { MatDividerModule } from '@angular/material/divider' +import { MatIconModule } from '@angular/material/icon' +import { MatProgressBarModule } from '@angular/material/progress-bar' import type { Column } from '@seed/api/column' import type { Cycle } from '@seed/api/cycle' -import { DataQualityService } from '@seed/api/data-quality'; +import { DataQualityService } from '@seed/api/data-quality' import type { ImportFile, MappingResultsResponse } from '@seed/api/dataset' import type { Organization } from '@seed/api/organization' import { ConfigService } from '@seed/services' @@ -21,8 +23,10 @@ import { Subject, switchMap, take } from 'rxjs' imports: [ AgGridAngular, CommonModule, - MatDividerModule, MatButtonModule, + MatDividerModule, + MatIconModule, + MatProgressBarModule, ], }) export class SaveMappingsComponent implements OnChanges, OnDestroy { diff --git a/src/app/modules/datasets/data-mappings/step4/match-merge.component.html b/src/app/modules/datasets/data-mappings/step4/match-merge.component.html index efbf94d5..fc40ebd0 100644 --- a/src/app/modules/datasets/data-mappings/step4/match-merge.component.html +++ b/src/app/modules/datasets/data-mappings/step4/match-merge.component.html @@ -4,6 +4,10 @@ [progress]="progressBarObj.progress" [total]="progressBarObj.total" [title]="progressBarObj.statusMessage" + [showSubProgress]="true" + [subProgress]="subProgressBarObj.progress" + [subTitle]="subProgressBarObj.statusMessage" + [subTotal]="subProgressBarObj.total" > } @else {
diff --git a/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts b/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts index e71257fb..3e2a06cd 100644 --- a/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts +++ b/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts @@ -4,10 +4,11 @@ import { Component, inject, Input } from '@angular/core' import { MatButtonModule } from '@angular/material/button' import { RouterModule } from '@angular/router' import { MappingService } from '@seed/api/mapping' -import { SubProgressResponse } from '@seed/api/progress' +import type { SubProgressResponse } from '@seed/api/progress' import { ProgressBarComponent } from '@seed/components' +import type { CheckProgressLoopParams} from '@seed/services/uploader' import { UploaderService } from '@seed/services/uploader' -import { Subject, switchMap, take } from 'rxjs' +import { finalize, Subject, switchMap, takeUntil, tap } from 'rxjs' @Component({ selector: 'seed-match-merge', @@ -35,33 +36,46 @@ export class MatchMergeComponent implements OnDestroy { startMatchMerge() { this._mappingService.mappingDone(this.orgId, this.importFileId) .pipe( - take(1), switchMap(() => this._mappingService.startMatchMerge(this.orgId, this.importFileId)), - take(1), switchMap((data) => this.checkProgress(data)), + takeUntil(this._unsubscribeAll$), ) .subscribe() } checkProgress(data: SubProgressResponse) { const successFn = () => { - console.log('success') this.inProgress = false } - const failureFn = () => { - console.log('failure') - } - - const { progress_data } = data - return this._uploaderService.checkProgressLoop({ + const { progress_data, sub_progress_data } = data + const baseParams = { offset: 0, multiplier: 1 } + const mainParams: CheckProgressLoopParams = { progressKey: progress_data.progress_key, - offset: 0, - multiplier: 1, successFn, - failureFn, + failureFn: () => void 0, progressBarObj: this.progressBarObj, - }) + ...baseParams, + } + + const subParams: CheckProgressLoopParams = { + progressKey: sub_progress_data.progress_key, + successFn: () => void 0, + failureFn: () => void 0, + progressBarObj: this.subProgressBarObj, + ...baseParams, + } + + return this._uploaderService.checkProgressLoopMainSub(mainParams, subParams) + .pipe( + tap(([_, subProgress]) => { + console.log('subProgress', subProgress.status_message, this.subProgressBarObj.statusMessage, this.subProgressBarObj.progress) + }), + finalize(() => { + console.log('final main progressBarObj', this.progressBarObj) + console.log('final sub progressBarObj', this.subProgressBarObj) + }), + ) } ngOnDestroy(): void { From f2d8998d2ee97cac8dbf4478052f9e25c88e89d5 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Thu, 3 Jul 2025 19:14:43 +0000 Subject: [PATCH 21/37] whole xls upload fxnal --- src/@seed/api/mapping/mapping.service.ts | 22 +++- src/@seed/api/mapping/mapping.types.ts | 26 ++++ .../step1/map-data.component.html | 18 +-- .../step4/match-merge.component.html | 13 +- .../step4/match-merge.component.ts | 27 ++-- .../step4/results.component.html | 66 ++++++++++ .../data-mappings/step4/results.component.ts | 119 ++++++++++++++++++ 7 files changed, 259 insertions(+), 32 deletions(-) create mode 100644 src/app/modules/datasets/data-mappings/step4/results.component.html create mode 100644 src/app/modules/datasets/data-mappings/step4/results.component.ts diff --git a/src/@seed/api/mapping/mapping.service.ts b/src/@seed/api/mapping/mapping.service.ts index 44197043..5e09ff0c 100644 --- a/src/@seed/api/mapping/mapping.service.ts +++ b/src/@seed/api/mapping/mapping.service.ts @@ -1,11 +1,12 @@ -import { HttpClient, HttpErrorResponse } from '@angular/common/http' +import type { HttpErrorResponse } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http' import { inject, Injectable } from '@angular/core' import { ErrorService } from '@seed/services' import { UserService } from '../user' import { catchError, map, tap, type Observable } from 'rxjs' -import type { FirstFiveRowsResponse, MappingSuggestionsResponse, RawColumnNamesResponse } from './mapping.types' +import type { FirstFiveRowsResponse, MappingSuggestionsResponse, MatchingResultsResponse, RawColumnNamesResponse } from './mapping.types' import { MappedData, MappingResultsResponse } from '../dataset' -import { ProgressResponse, SubProgressResponse } from '../progress' +import type { ProgressResponse, SubProgressResponse } from '../progress' @Injectable({ providedIn: 'root' }) export class MappingService { @@ -88,9 +89,10 @@ export class MappingService { ) } - startMatchMerge(orgId: number, importFileId: number): Observable { + startMatchMerge(orgId: number, importFileId: number): Observable { const url = `/api/v3/import_files/${importFileId}/start_system_matching_and_geocoding/?organization_id=${orgId}` - return this._httpClient.post(url, {}) + // returns ProgressResponse if already matched + return this._httpClient.post(url, {}) .pipe( tap((response) => { console.log('Match merge started:', response) @@ -100,4 +102,14 @@ export class MappingService { }), ) } + + getMatchingResults(orgId: number, importFileId: number): Observable { + const url = `/api/v3/import_files/${importFileId}/matching_and_geocoding_results/?organization_id=${orgId}` + return this._httpClient.get(url) + .pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error getting matching and geocoding results') + }), + ) + } } diff --git a/src/@seed/api/mapping/mapping.types.ts b/src/@seed/api/mapping/mapping.types.ts index 170a7d78..c5db1412 100644 --- a/src/@seed/api/mapping/mapping.types.ts +++ b/src/@seed/api/mapping/mapping.types.ts @@ -18,3 +18,29 @@ export type FirstFiveRowsResponse = { status: string; first_five_rows: Record[]; } + +export type MatchingResultsResponse = { + import_file_records: number; + multiple_cycle_upload: boolean; + properties: MatchingResults; + tax_lots: MatchingResults; +} + +export type MatchingResults = { + duplicates_against_existing: number; + duplicates_within_file: number; + duplicates_within_file_errors: number; + geocode_not_possible: number; + geocoded_census_geocoder: number; + geocoded_high_confidence: number; + geocoded_low_confidence: number; + geocoded_manually: number; + initial_incoming: number; + merges_against_existing: number; + merges_against_existing_errors: number; + merges_between_existing: number; + merges_within_file: number; + merges_within_file_errors: number; + new: number; + new_errors: number; +} diff --git a/src/app/modules/datasets/data-mappings/step1/map-data.component.html b/src/app/modules/datasets/data-mappings/step1/map-data.component.html index 7e3ae3ec..a8ac3c5e 100644 --- a/src/app/modules/datasets/data-mappings/step1/map-data.component.html +++ b/src/app/modules/datasets/data-mappings/step1/map-data.component.html @@ -1,15 +1,17 @@ -
-
Cycle
-
{{ cycle?.name }}
-
-
-
Column Profile
-
none selected
+
+
+
Cycle:
+
{{ cycle?.name }}
+
+
+
Column Profile:
+
none selected
+
- + Property Types diff --git a/src/app/modules/datasets/data-mappings/step4/match-merge.component.html b/src/app/modules/datasets/data-mappings/step4/match-merge.component.html index fc40ebd0..cb23f6f5 100644 --- a/src/app/modules/datasets/data-mappings/step4/match-merge.component.html +++ b/src/app/modules/datasets/data-mappings/step4/match-merge.component.html @@ -10,12 +10,11 @@ [subTotal]="subProgressBarObj.total" > } @else { - + }
\ No newline at end of file diff --git a/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts b/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts index 3e2a06cd..a357adea 100644 --- a/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts +++ b/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts @@ -3,12 +3,13 @@ import type { OnDestroy } from '@angular/core' import { Component, inject, Input } from '@angular/core' import { MatButtonModule } from '@angular/material/button' import { RouterModule } from '@angular/router' +import { of, Subject, switchMap, takeUntil } from 'rxjs' import { MappingService } from '@seed/api/mapping' -import type { SubProgressResponse } from '@seed/api/progress' +import type { ProgressResponse, SubProgressResponse } from '@seed/api/progress' import { ProgressBarComponent } from '@seed/components' import type { CheckProgressLoopParams} from '@seed/services/uploader' import { UploaderService } from '@seed/services/uploader' -import { finalize, Subject, switchMap, takeUntil, tap } from 'rxjs' +import { ResultsComponent } from './results.component' @Component({ selector: 'seed-match-merge', @@ -18,6 +19,7 @@ import { finalize, Subject, switchMap, takeUntil, tap } from 'rxjs' MatButtonModule, ProgressBarComponent, RouterModule, + ResultsComponent, ], }) export class MatchMergeComponent implements OnDestroy { @@ -34,15 +36,25 @@ export class MatchMergeComponent implements OnDestroy { subProgressBarObj = this._uploaderService.defaultProgressBarObj startMatchMerge() { + this.inProgress = true this._mappingService.mappingDone(this.orgId, this.importFileId) .pipe( switchMap(() => this._mappingService.startMatchMerge(this.orgId, this.importFileId)), - switchMap((data) => this.checkProgress(data)), + switchMap((response) => this.checkProgressResponse(response)), takeUntil(this._unsubscribeAll$), ) .subscribe() } + checkProgressResponse(response: ProgressResponse | SubProgressResponse) { + // check if its already matched and skip progress step + if ((response as ProgressResponse).progress === 100) { + this.inProgress = false + return of(null) + } + return this.checkProgress(response as SubProgressResponse) + } + checkProgress(data: SubProgressResponse) { const successFn = () => { this.inProgress = false @@ -67,15 +79,6 @@ export class MatchMergeComponent implements OnDestroy { } return this._uploaderService.checkProgressLoopMainSub(mainParams, subParams) - .pipe( - tap(([_, subProgress]) => { - console.log('subProgress', subProgress.status_message, this.subProgressBarObj.statusMessage, this.subProgressBarObj.progress) - }), - finalize(() => { - console.log('final main progressBarObj', this.progressBarObj) - console.log('final sub progressBarObj', this.subProgressBarObj) - }), - ) } ngOnDestroy(): void { diff --git a/src/app/modules/datasets/data-mappings/step4/results.component.html b/src/app/modules/datasets/data-mappings/step4/results.component.html new file mode 100644 index 00000000..8cc9c5e9 --- /dev/null +++ b/src/app/modules/datasets/data-mappings/step4/results.component.html @@ -0,0 +1,66 @@ +
+ + @if ( matchingResults) { + + +
+ Records found: {{ matchingResults?.import_file_records }} +
+ } @else { +
+
+ +
Getting Results...
+
+ +
+ } + + @if (hasPropertyData) { +
+ +
+ +
+
+ + Properties +
+ + + +
+ } + + @if (hasTaxlotData) { + +
+
+ + Tax Lots +
+ + + +
+ } + + +
\ No newline at end of file diff --git a/src/app/modules/datasets/data-mappings/step4/results.component.ts b/src/app/modules/datasets/data-mappings/step4/results.component.ts new file mode 100644 index 00000000..2724e94e --- /dev/null +++ b/src/app/modules/datasets/data-mappings/step4/results.component.ts @@ -0,0 +1,119 @@ +import { CommonModule } from '@angular/common' +import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core' +import { Component, inject, Input } from '@angular/core' +import { MatButtonModule } from '@angular/material/button' +import { MatDividerModule } from '@angular/material/divider' +import { MatIconModule } from '@angular/material/icon' +import { MatProgressBarModule } from '@angular/material/progress-bar' +import { RouterModule } from '@angular/router' +import { AgGridAngular } from 'ag-grid-angular' +import type { ColDef } from 'ag-grid-community' +import { Subject, takeUntil, tap } from 'rxjs' +import type { MatchingResultsResponse } from '@seed/api/mapping' +import { MappingService } from '@seed/api/mapping' +import { ConfigService } from '@seed/services' +import type { InventoryType } from 'app/modules/inventory' + +@Component({ + selector: 'seed-match-merge-results', + templateUrl: './results.component.html', + imports: [ + AgGridAngular, + CommonModule, + MatButtonModule, + MatIconModule, + MatDividerModule, + MatProgressBarModule, + RouterModule, + ], +}) +export class ResultsComponent implements OnChanges, OnDestroy { + @Input() importFileId: number + @Input() orgId: number + @Input() inventoryType: InventoryType + @Input() inProgress = true + + private _configService = inject(ConfigService) + private _mappingService = inject(MappingService) + private readonly _unsubscribeAll$ = new Subject() + + gridTheme$ = this._configService.gridTheme$ + generalData: Record[] + generalColDefs: ColDef[] = [] + inventoryColDefs: ColDef[] = [ + { field: 'status', headerName: 'Status' }, + { field: 'count', headerName: 'Count' }, + ] + matchingResults: MatchingResultsResponse + propertyData: Record[] = [] + taxlotData: Record[] = [] + hasPropertyData = false + hasTaxlotData = false + + ngOnChanges(changes: SimpleChanges): void { + console.log('changes', changes) + if (changes.inProgress.currentValue === false) { + this.getMatchingResults() + } + } + + getMatchingResults() { + this._mappingService.getMatchingResults(this.orgId, this.importFileId) + .pipe( + takeUntil(this._unsubscribeAll$), + tap((results) => { this.setGrid(results) }), + ) + .subscribe() + } + + setGrid(results: MatchingResultsResponse) { + this.matchingResults = results + this.setGeneralGrid() + this.setInventoryGrids() + } + + setGeneralGrid() { + this.generalColDefs = [ + { field: 'import_file_records', headerName: 'Records in File' }, + { field: 'multiple_cycle_upload', headerName: 'Multi Cycle Upload' }, + ] + const { import_file_records, multiple_cycle_upload } = this.matchingResults + this.generalData = [{ import_file_records, multiple_cycle_upload }] + } + + setInventoryGrids() { + this.setPropertyData() + this.setTaxLotData() + } + + setPropertyData() { + const { properties } = this.matchingResults + this.hasPropertyData = Object.values(properties).some((v) => v) + this.propertyData = Object.entries(properties) + .filter(([_, v]) => v) + .map(([k, v]) => ({ + status: this.readableString(k), + count: v, + })) + } + + setTaxLotData() { + const { tax_lots } = this.matchingResults + this.hasTaxlotData = Object.values(tax_lots).some((v) => v) + this.taxlotData = Object.entries(tax_lots) + .filter(([_, v]) => v) + .map(([k, v]) => ({ + status: this.readableString(k), + count: v, + })) + } + + readableString(str: string) { + return str.replace(/_/g, ' ').replace(/^\w/, (c) => c.toUpperCase()) + } + + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } +} From cd6f9f0861e115a2dcd55dd270431291b378ca30 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Thu, 3 Jul 2025 19:20:23 +0000 Subject: [PATCH 22/37] styles --- src/@seed/components/progress/progress-bar.component.html | 2 +- .../data-mappings/step3/save-mappings.component.html | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/@seed/components/progress/progress-bar.component.html b/src/@seed/components/progress/progress-bar.component.html index d8aa727f..8ffeecdd 100644 --- a/src/@seed/components/progress/progress-bar.component.html +++ b/src/@seed/components/progress/progress-bar.component.html @@ -12,7 +12,7 @@ }
- +
@if (showSubProgress && subProgress && subProgress < 100) { diff --git a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html index dcef41f0..a5f07b0c 100644 --- a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html +++ b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html @@ -1,6 +1,6 @@ -
-
Cycle
-
{{ cycle?.name }}
+
+
Cycle:
+
{{ cycle?.name }}
From 75f08643be92ecf8386b581efdc31cb5c2bb3524 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Thu, 3 Jul 2025 19:28:40 +0000 Subject: [PATCH 23/37] autocomplete not subset --- src/@seed/components/ag-grid/autocomplete.component.ts | 3 +-- src/app/modules/datasets/data-mappings/step1/column-defs.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/@seed/components/ag-grid/autocomplete.component.ts b/src/@seed/components/ag-grid/autocomplete.component.ts index 1cfa9db2..c842afde 100644 --- a/src/@seed/components/ag-grid/autocomplete.component.ts +++ b/src/@seed/components/ag-grid/autocomplete.component.ts @@ -6,7 +6,6 @@ import { MatFormFieldModule } from '@angular/material/form-field' import { MatInputModule } from '@angular/material/input' import type { ICellEditorAngularComp } from 'ag-grid-angular' import type { ICellEditorParams } from 'ag-grid-community' -import { isOrderedSubset } from '@seed/utils/string-matching.util' @Component({ selector: 'seed-ag-grid-auto-complete-cell', @@ -35,7 +34,7 @@ export class AutocompleteCellComponent implements ICellEditorAngularComp, AfterV this.inputCtrl.valueChanges.subscribe((value) => { // autocomplete this.filteredOptions = this.options.filter((option) => { - return isOrderedSubset(value, option) + return option.toLowerCase().startsWith(value.toLowerCase()) }) }) } diff --git a/src/app/modules/datasets/data-mappings/step1/column-defs.ts b/src/app/modules/datasets/data-mappings/step1/column-defs.ts index 86113131..e1f01159 100644 --- a/src/app/modules/datasets/data-mappings/step1/column-defs.ts +++ b/src/app/modules/datasets/data-mappings/step1/column-defs.ts @@ -1,6 +1,6 @@ +import type { CellValueChangedEvent, ColDef, ColGroupDef, ICellRendererParams } from 'ag-grid-community' import { EditHeaderComponent } from '@seed/components' import { AutocompleteCellComponent } from '@seed/components/ag-grid/autocomplete.component' -import type { CellValueChangedEvent, ColDef, ColGroupDef, ICellRendererParams } from 'ag-grid-community' import { dataTypeOptions, unitMap } from './constants' export const gridOptions = { From 90cc539286b013fee0d2b3ec6c8932eaea671307 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Thu, 3 Jul 2025 19:47:57 +0000 Subject: [PATCH 24/37] steps not editable --- .../datasets/data-mappings/data-mapping.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.html b/src/app/modules/datasets/data-mappings/data-mapping.component.html index cb7092e9..0016eab4 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.html +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.html @@ -29,7 +29,7 @@ - + - + Date: Mon, 7 Jul 2025 14:38:37 +0000 Subject: [PATCH 25/37] select all count --- src/app/modules/inventory-list/list/grid/actions.component.ts | 2 ++ src/app/modules/inventory-list/list/inventory.component.html | 1 + src/app/modules/inventory-list/list/inventory.component.ts | 4 ++++ 3 files changed, 7 insertions(+) diff --git a/src/app/modules/inventory-list/list/grid/actions.component.ts b/src/app/modules/inventory-list/list/grid/actions.component.ts index 46db90e6..36efb584 100644 --- a/src/app/modules/inventory-list/list/grid/actions.component.ts +++ b/src/app/modules/inventory-list/list/grid/actions.component.ts @@ -26,6 +26,7 @@ export class ActionsComponent implements OnDestroy { @Input() selectedViewIds: number[] @Input() type: InventoryType @Output() refreshInventory = new EventEmitter() + @Output() selectedAll = new EventEmitter() private _inventoryService = inject(InventoryService) private _dialog = inject(MatDialog) private readonly _unsubscribeAll$ = new Subject() @@ -95,6 +96,7 @@ export class ActionsComponent implements OnDestroy { const paramString = params.toString() this._inventoryService.getAgInventory(paramString, {}).subscribe(({ results }: { results: number[] }) => { this.selectedViewIds = results + this.selectedAll.emit(this.selectedViewIds) }) } diff --git a/src/app/modules/inventory-list/list/inventory.component.html b/src/app/modules/inventory-list/list/inventory.component.html index 06950a1f..62137c08 100644 --- a/src/app/modules/inventory-list/list/inventory.component.html +++ b/src/app/modules/inventory-list/list/inventory.component.html @@ -23,6 +23,7 @@ [selectedViewIds]="selectedViewIds" [type]="type" (refreshInventory)="refreshInventory$.next()" + (selectedAll)="onSelectAll($event)" > diff --git a/src/app/modules/inventory-list/list/inventory.component.ts b/src/app/modules/inventory-list/list/inventory.component.ts index fc590ca7..f84648ce 100644 --- a/src/app/modules/inventory-list/list/inventory.component.ts +++ b/src/app/modules/inventory-list/list/inventory.component.ts @@ -271,6 +271,10 @@ export class InventoryComponent implements OnDestroy, OnInit { this.selectedViewIds = this.gridApi.getSelectedRows().map(({ property_view_id }: { property_view_id: number }) => property_view_id) } + onSelectAll(selectedViewIds: number[]) { + this.selectedViewIds = selectedViewIds + } + onProfileChange(id: number) { this.profileId$.next(id) } From f380c2fcd5685d1718768add93c2ba0b46fdaf02 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Mon, 7 Jul 2025 14:42:40 +0000 Subject: [PATCH 26/37] rounded btns --- src/app/modules/datasets/dataset/dataset.component.ts | 4 ++-- src/app/modules/datasets/datasets.component.ts | 8 ++++---- .../sensors/data-loggers/data-loggers-grid.component.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/modules/datasets/dataset/dataset.component.ts b/src/app/modules/datasets/dataset/dataset.component.ts index 480108eb..799af594 100644 --- a/src/app/modules/datasets/dataset/dataset.component.ts +++ b/src/app/modules/datasets/dataset/dataset.component.ts @@ -95,11 +95,11 @@ export class DatasetComponent implements OnDestroy, OnInit { actionsRenderer() { return `
- + Data Mapping open_in_new - + Data Pairing open_in_new diff --git a/src/app/modules/datasets/datasets.component.ts b/src/app/modules/datasets/datasets.component.ts index 9d1acba2..4c10365d 100644 --- a/src/app/modules/datasets/datasets.component.ts +++ b/src/app/modules/datasets/datasets.component.ts @@ -8,15 +8,15 @@ import { ActivatedRoute, Router } from '@angular/router' import { AgGridAngular } from 'ag-grid-angular' import type { CellClickedEvent, ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' import { filter, switchMap, tap } from 'rxjs' +import type { Cycle } from '@seed/api/cycle' +import { CycleService } from '@seed/api/cycle/cycle.service' import { type Dataset, DatasetService } from '@seed/api/dataset' import { UserService } from '@seed/api/user' import { DeleteModalComponent, PageComponent } from '@seed/components' import { ConfigService } from '@seed/services' import { naturalSort } from '@seed/utils' -import { FormModalComponent } from './modal/form-modal.component' import { UploadFileModalComponent } from './data-upload/data-upload-modal.component' -import { CycleService } from '@seed/api/cycle/cycle.service' -import { Cycle } from '@seed/api/cycle' +import { FormModalComponent } from './modal/form-modal.component' @Component({ selector: 'seed-data', @@ -95,7 +95,7 @@ export class DatasetsComponent implements OnInit { actionsRenderer() { return `
- + add Data Files diff --git a/src/app/modules/inventory-detail/sensors/data-loggers/data-loggers-grid.component.ts b/src/app/modules/inventory-detail/sensors/data-loggers/data-loggers-grid.component.ts index ef37f423..678c8779 100644 --- a/src/app/modules/inventory-detail/sensors/data-loggers/data-loggers-grid.component.ts +++ b/src/app/modules/inventory-detail/sensors/data-loggers/data-loggers-grid.component.ts @@ -74,11 +74,11 @@ export class DataLoggersGridComponent implements OnChanges { actionRenderer() { return `
- + add Sensors - + add Readings From 5aacc7b69418aee723188a8966c3176eeaf1fa5c Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Mon, 7 Jul 2025 17:03:17 +0000 Subject: [PATCH 27/37] styles --- .../ag-grid/autocomplete.component.html | 1 - .../ag-grid/autocomplete.component.ts | 8 ------ .../step1/map-data.component.html | 27 ++++++++++++++---- .../data-mappings/step1/map-data.component.ts | 26 +++++++++++------ .../datasets/dataset/dataset.component.ts | 2 +- .../modal/more-actions-modal.component.html | 8 ++++-- .../modal/more-actions-modal.component.ts | 28 ++++++++++--------- src/styles/styles.scss | 4 +++ 8 files changed, 65 insertions(+), 39 deletions(-) diff --git a/src/@seed/components/ag-grid/autocomplete.component.html b/src/@seed/components/ag-grid/autocomplete.component.html index db0592f0..61e84dd6 100644 --- a/src/@seed/components/ag-grid/autocomplete.component.html +++ b/src/@seed/components/ag-grid/autocomplete.component.html @@ -4,7 +4,6 @@ matInput [formControl]="inputCtrl" [matAutocomplete]="auto" - (keydown)="onKeyDown($event)" /> @for (option of filteredOptions; track $index) { diff --git a/src/@seed/components/ag-grid/autocomplete.component.ts b/src/@seed/components/ag-grid/autocomplete.component.ts index c842afde..d54a4e4b 100644 --- a/src/@seed/components/ag-grid/autocomplete.component.ts +++ b/src/@seed/components/ag-grid/autocomplete.component.ts @@ -48,12 +48,4 @@ export class AutocompleteCellComponent implements ICellEditorAngularComp, AfterV this.input.nativeElement.focus() }) } - - onKeyDown(event: KeyboardEvent) { - // if enter, accept the value and stop propagation - const exitKeys = ['Enter'] - if (!exitKeys.includes(event.key)) { - event.stopPropagation() - } - } } diff --git a/src/app/modules/datasets/data-mappings/step1/map-data.component.html b/src/app/modules/datasets/data-mappings/step1/map-data.component.html index a8ac3c5e..5b92c7f4 100644 --- a/src/app/modules/datasets/data-mappings/step1/map-data.component.html +++ b/src/app/modules/datasets/data-mappings/step1/map-data.component.html @@ -1,14 +1,19 @@ -
+ +
+
-
Cycle:
+
Cycle:
{{ cycle?.name }}
+
-
Column Profile:
+
Column Profile:
none selected
+
+
@@ -19,6 +24,7 @@
+
@@ -26,9 +32,20 @@
Editable Cell
- - + +
+ @if (errorMessages.length ) { + +
    + @for (error of errorMessages; track $index) { +
  • {{ error }}
  • + } +
+
+ } +
+
+ errorMessages: string[] = [] fileId = this._router.snapshot.params.id as number gridApi: GridApi gridOptions = gridOptions @@ -216,6 +218,7 @@ export class MapDataComponent implements OnChanges, OnDestroy { } validateData() { + this.errorMessages = [] const matchingColumns = this.defaultInventoryType === 'Tax Lot' ? this.matchingTaxLotColumns : this.matchingPropertyColumns const toFields = [] this.gridApi.forEachNode((node: RowNode) => { @@ -223,19 +226,26 @@ export class MapDataComponent implements OnChanges, OnDestroy { toFields.push(node.data.to_field) }) - // no duplicates - if (toFields.length !== new Set(toFields).size) { - this.dataValid = false - return - } // at least one matching column const hasMatchingCol = toFields.some((col) => matchingColumns.includes(col)) if (!hasMatchingCol) { + const matchingColNames = this.columns.filter((c) => c.is_matching_criteria).map((c) => c.display_name).join(', ') + this.errorMessages.push(`At least one of the following Property fields is required: ${matchingColNames}.`) + } + + // all fields must be mapped (no empty fields) + if (!toFields.every((f) => f)) { + this.dataValid = false + this.errorMessages.push('All SEED Headers must be mapped. Empty values are not allowed.') + } + + // no duplicates + if (toFields.length !== new Set(toFields).size) { this.dataValid = false - return + this.errorMessages.push('Duplicate headers found. Each SEED Header must be unique.') } - this.dataValid = true + this.dataValid = this.errorMessages.length === 0 } ngOnDestroy(): void { diff --git a/src/app/modules/datasets/dataset/dataset.component.ts b/src/app/modules/datasets/dataset/dataset.component.ts index 799af594..1b283ef7 100644 --- a/src/app/modules/datasets/dataset/dataset.component.ts +++ b/src/app/modules/datasets/dataset/dataset.component.ts @@ -99,7 +99,7 @@ export class DatasetComponent implements OnDestroy, OnInit { Data Mapping open_in_new - + Data Pairing open_in_new diff --git a/src/app/modules/inventory-list/list/modal/more-actions-modal.component.html b/src/app/modules/inventory-list/list/modal/more-actions-modal.component.html index 3d51d6d5..07bc471a 100644 --- a/src/app/modules/inventory-list/list/modal/more-actions-modal.component.html +++ b/src/app/modules/inventory-list/list/modal/more-actions-modal.component.html @@ -1,6 +1,8 @@ -

- Apply Action to Selection ({{ data.viewIds.length }}) -

+
+ +
Apply Action to Selection ({{ data.viewIds.length }})
+
+
    diff --git a/src/app/modules/inventory-list/list/modal/more-actions-modal.component.ts b/src/app/modules/inventory-list/list/modal/more-actions-modal.component.ts index 3c3dc7ef..d9622e12 100644 --- a/src/app/modules/inventory-list/list/modal/more-actions-modal.component.ts +++ b/src/app/modules/inventory-list/list/modal/more-actions-modal.component.ts @@ -2,12 +2,14 @@ import type { OnDestroy } from '@angular/core' import { Component, inject } from '@angular/core' import { MatButtonModule } from '@angular/material/button' import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog' +import { MatDividerModule } from '@angular/material/divider' +import { MatIconModule } from '@angular/material/icon' import { Subject } from 'rxjs' @Component({ selector: 'seed-inventory-more-actions-modal', templateUrl: './more-actions-modal.component.html', - imports: [MatButtonModule, MatDialogModule], + imports: [MatButtonModule, MatDialogModule, MatDividerModule, MatIconModule], }) export class MoreActionsModalComponent implements OnDestroy { private _dialogRef = inject(MatDialogRef) @@ -17,27 +19,27 @@ export class MoreActionsModalComponent implements OnDestroy { errorMessage = false actionsColumn1 = [ - { name: 'Add / Remove Groups', action: this.tempAction, disabled: !this.data.viewIds.length }, - { name: 'Add / Remove Labels', action: this.tempAction, disabled: !this.data.viewIds.length }, - { name: 'Add / Update UBID', action: this.tempAction, disabled: !this.data.viewIds.length }, - { name: 'Change ALI', action: this.tempAction, disabled: !this.data.viewIds.length }, - { name: 'Compare UBID', action: this.tempAction, disabled: !this.data.viewIds.length }, + { name: 'Analysis: Run', action: this.tempAction, disabled: !this.data.viewIds.length }, + { name: 'Audit Template: Export', action: this.tempAction, disabled: !this.data.viewIds.length }, + { name: 'Audit Template: Update', action: this.tempAction, disabled: !this.data.viewIds.length }, + { name: 'Change Access Level', action: this.tempAction, disabled: !this.data.viewIds.length }, { name: 'Data Quality Check', action: this.tempAction, disabled: !this.data.viewIds.length }, - { name: 'Decode UBID', action: this.tempAction, disabled: !this.data.viewIds.length }, { name: 'Delete', action: this.tempAction, disabled: !this.data.viewIds.length }, + { name: 'Derived Data: Update', action: this.tempAction, disabled: !this.data.viewIds.length }, { name: 'Email', action: this.tempAction, disabled: !this.data.viewIds.length }, { name: 'Export', action: this.tempAction, disabled: !this.data.viewIds.length }, + { name: 'FEMP CTS Reporting Export', action: this.tempAction, disabled: !this.data.viewIds.length }, ] actionsColumn2 = [ - { name: 'Export to AT', action: this.tempAction, disabled: !this.data.viewIds.length }, - { name: 'FEMP CTS Reporting Export', action: this.tempAction, disabled: !this.data.viewIds.length }, { name: 'Geocode', action: this.tempAction, disabled: !this.data.viewIds.length }, + { name: 'Groups: Add / Remove', action: this.tempAction, disabled: !this.data.viewIds.length }, + { name: 'Labels: Add / Remove', action: this.tempAction, disabled: !this.data.viewIds.length }, { name: 'Merge', action: this.tempAction, disabled: !this.data.viewIds.length }, - { name: 'Run Analysis', action: this.tempAction, disabled: !this.data.viewIds.length }, { name: 'Set Update Time to Now', action: this.tempAction, disabled: !this.data.viewIds.length }, - { name: 'Update Derived Data', action: this.tempAction, disabled: !this.data.viewIds.length }, - { name: 'Update Salesforce', action: this.tempAction, disabled: !this.data.viewIds.length }, - { name: 'Update with AT', action: this.tempAction, disabled: !this.data.viewIds.length }, + { name: 'Salesforce: Update', action: this.tempAction, disabled: !this.data.viewIds.length }, + { name: 'UBID: Add / Update', action: this.tempAction, disabled: !this.data.viewIds.length }, + { name: 'UBID: Compare', action: this.tempAction, disabled: !this.data.viewIds.length }, + { name: 'UBID: Decode', action: this.tempAction, disabled: !this.data.viewIds.length }, ] tempAction() { diff --git a/src/styles/styles.scss b/src/styles/styles.scss index ea3bedf1..eabaf340 100644 --- a/src/styles/styles.scss +++ b/src/styles/styles.scss @@ -213,3 +213,7 @@ border-radius: 25px !important; } } + +.vertical-divider { + @apply border +} From e4b8b2a3a6e53787abaf880de36a0d3270472934 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Mon, 7 Jul 2025 20:10:23 +0000 Subject: [PATCH 28/37] data quality results modal --- .../api/data-quality/data-quality.service.ts | 25 +++- .../api/data-quality/data-quality.types.ts | 21 ++++ src/app/modules/data-quality/index.ts | 1 + .../data-quality/results-modal.component.html | 24 ++++ .../data-quality/results-modal.component.ts | 118 ++++++++++++++++++ .../step3/save-mappings.component.ts | 11 +- .../list/grid/actions.component.ts | 2 +- .../modal/more-actions-modal.component.html | 76 ++++++----- .../modal/more-actions-modal.component.ts | 64 ++++++++-- 9 files changed, 297 insertions(+), 45 deletions(-) create mode 100644 src/app/modules/data-quality/index.ts create mode 100644 src/app/modules/data-quality/results-modal.component.html create mode 100644 src/app/modules/data-quality/results-modal.component.ts diff --git a/src/@seed/api/data-quality/data-quality.service.ts b/src/@seed/api/data-quality/data-quality.service.ts index 9202dc71..262dce0e 100644 --- a/src/@seed/api/data-quality/data-quality.service.ts +++ b/src/@seed/api/data-quality/data-quality.service.ts @@ -1,11 +1,11 @@ import { HttpClient, type HttpErrorResponse } from '@angular/common/http' import { inject, Injectable } from '@angular/core' -import { catchError, type Observable, ReplaySubject, switchMap, tap } from 'rxjs' +import { catchError, map, type Observable, ReplaySubject, switchMap, tap } from 'rxjs' import { OrganizationService } from '@seed/api/organization' import { ErrorService } from '@seed/services' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' -import type { Rule } from './data-quality.types' -import { DQCProgressResponse } from '../progress' +import type { DataQualityResults, DataQualityResultsResponse, Rule } from './data-quality.types' +import type { DQCProgressResponse } from '../progress' @Injectable({ providedIn: 'root' }) export class DataQualityService { @@ -91,9 +91,24 @@ export class DataQualityService { ) } - getDataQualityResults(orgId: number, runId: number): Observable { + startDataQualityCheckForOrg(orgId: number, property_view_ids: number[], taxlot_view_ids: number[], goal_id: number): Observable { + const url = `/api/v3/data_quality_checks/${orgId}/start/` + const data = { + property_view_ids, + taxlot_view_ids, + goal_id, + } + return this._httpClient.post(url, data).pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error fetching data quality results for organization') + }), + ) + } + + getDataQualityResults(orgId: number, runId: number): Observable { const url = `/api/v3/data_quality_checks/results/?organization_id=${orgId}&run_id=${runId}` - return this._httpClient.get(url).pipe( + return this._httpClient.get(url).pipe( + map(({ data }) => data), catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error fetching data quality results') }), diff --git a/src/@seed/api/data-quality/data-quality.types.ts b/src/@seed/api/data-quality/data-quality.types.ts index f1034fb3..5291b421 100644 --- a/src/@seed/api/data-quality/data-quality.types.ts +++ b/src/@seed/api/data-quality/data-quality.types.ts @@ -60,3 +60,24 @@ export type UnitNames = | 'MJ/m²/year' | 'kWh/m²/year' | 'kBtu/m²/year' + +export type DataQualityResultsResponse = { + data: DataQualityResults[]; +} + +export type DataQualityResults = { + [key: string]: unknown; + data_quality_results: DataQualityResult[]; +} + +export type DataQualityResult = { + condition: string; + detailed_message: string; + field: string; + formatted_field: string; + label: string; + message: string; + severity: string; + table_name: string; + value: unknown; +} diff --git a/src/app/modules/data-quality/index.ts b/src/app/modules/data-quality/index.ts new file mode 100644 index 00000000..33a55835 --- /dev/null +++ b/src/app/modules/data-quality/index.ts @@ -0,0 +1 @@ +export * from './results-modal.component' diff --git a/src/app/modules/data-quality/results-modal.component.html b/src/app/modules/data-quality/results-modal.component.html new file mode 100644 index 00000000..9bcc354f --- /dev/null +++ b/src/app/modules/data-quality/results-modal.component.html @@ -0,0 +1,24 @@ +
    + +
    Data Quality Results
    +
    + +
    + @if (rowData.length) { + + + } @else { +
    No warnings or errors
    + } +
    + +
    + +
    \ No newline at end of file diff --git a/src/app/modules/data-quality/results-modal.component.ts b/src/app/modules/data-quality/results-modal.component.ts new file mode 100644 index 00000000..64fd81be --- /dev/null +++ b/src/app/modules/data-quality/results-modal.component.ts @@ -0,0 +1,118 @@ +import { CommonModule } from '@angular/common' +import type { OnDestroy, OnInit } from '@angular/core' +import { Component, inject } from '@angular/core' +import { MatButtonModule } from '@angular/material/button' +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' +import { MatDividerModule } from '@angular/material/divider' +import { MatIconModule } from '@angular/material/icon' +import type { DataQualityResults } from '@seed/api/data-quality' +import { DataQualityService } from '@seed/api/data-quality' +import { ConfigService } from '@seed/services' +import { AgGridAngular } from 'ag-grid-angular' +import type { ColDef } from 'ag-grid-community' +import { Subject, takeUntil, tap } from 'rxjs' + +@Component({ + selector: 'seed-data-quality-results', + templateUrl: './results-modal.component.html', + imports: [ + AgGridAngular, + CommonModule, + MatIconModule, + MatButtonModule, + MatDividerModule, + ], +}) +export class ResultsModalComponent implements OnDestroy, OnInit { + private readonly _unsubscribeAll$ = new Subject() + private _configService = inject(ConfigService) + private _dataQualityService = inject(DataQualityService) + private _dialog = inject(MatDialogRef) + + data = inject(MAT_DIALOG_DATA) as { orgId: number; dqcId: number } + + columnDefs: ColDef[] + gridTheme$ = this._configService.gridTheme$ + rowData: Record[] = [] + results: DataQualityResults[] = [] + + ngOnInit() { + this._dataQualityService.getDataQualityResults(this.data.orgId, this.data.dqcId) + .pipe( + takeUntil(this._unsubscribeAll$), + tap((results) => { + this.results = results + this.setGrid() + }), + ) + .subscribe() + } + + setGrid() { + if (this.results.length) { + this.setColumnDefs() + this.setRowData() + } + } + + setColumnDefs() { + const excludeKeys = ['id', 'data_quality_results'] + const keys = Object.keys(this.results[0]).filter((key) => !excludeKeys.includes(key)) + const matchingColDefs = keys.map((key) => ({ + field: key, + headerName: key.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()), + })) + + const styleLookup: Record = { + error: 'bg-red-600 text-white', + warning: 'bg-amber-500 text-white', + } + + const resultDefs = [ + { field: 'table_name', headerName: 'Table' }, + { field: 'formatted_field', headerName: 'Field' }, + { field: 'label', headerName: 'Applied Label' }, + { field: 'severity', hide: true }, + { + field: 'detailed_message', + headerName: 'Error Message', + cellClass: ({ data }: { data: { severity: string } }) => { + return styleLookup[data?.severity] || '' + }, + }, + ] + + this.columnDefs = [...matchingColDefs, ...resultDefs] + } + + setRowData() { + this.rowData = [] + const excludeKeys = ['id', 'data_quality_results'] + const keys = Object.keys(this.results[0]).filter((key) => !excludeKeys.includes(key)) + + for (const result of this.results) { + const matchingData = this.formatMatchingColData(keys, result) + for (const dqc of result.data_quality_results) { + const data = { ...matchingData, ...dqc } + this.rowData.push(data) + } + } + } + + formatMatchingColData(keys: string[], result: Record) { + const data = {} + for (const key of keys) { + data[key] = result[key] + } + return data + } + + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } + + dismiss() { + this._dialog.close() + } +} diff --git a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts index 65bb5c15..82d4c444 100644 --- a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts +++ b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common' import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, inject, Input, Output } from '@angular/core' import { MatButtonModule } from '@angular/material/button' +import { MatDialog } from '@angular/material/dialog'; import { MatDividerModule } from '@angular/material/divider' import { MatIconModule } from '@angular/material/icon' import { MatProgressBarModule } from '@angular/material/progress-bar' @@ -14,8 +15,9 @@ import { ConfigService } from '@seed/services' import { UploaderService } from '@seed/services/uploader' import { AgGridAngular } from 'ag-grid-angular' import type { ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' +import { ResultsModalComponent } from 'app/modules/data-quality'; import type { InventoryType } from 'app/modules/inventory' -import { Subject, switchMap, take } from 'rxjs' +import { Subject, switchMap, take, tap } from 'rxjs' @Component({ selector: 'seed-save-mappings', @@ -41,6 +43,7 @@ export class SaveMappingsComponent implements OnChanges, OnDestroy { private _configService = inject(ConfigService) private _dataQualityService = inject(DataQualityService) + private _dialog = inject(MatDialog) private _uploaderService = inject(UploaderService) private _unsubscribeAll$ = new Subject() columnDefs: ColDef[] = [] @@ -49,6 +52,7 @@ export class SaveMappingsComponent implements OnChanges, OnDestroy { gridTheme$ = this._configService.gridTheme$ mappingResults: Record[] = [] dqcComplete = false + dqcId: number inventoryType: InventoryType progressBarObj = this._uploaderService.defaultProgressBarObj @@ -89,6 +93,7 @@ export class SaveMappingsComponent implements OnChanges, OnDestroy { progressBarObj: this.progressBarObj, }) }), + tap(({ unique_id }) => { this.dqcId = unique_id }), ) .subscribe() } @@ -139,6 +144,10 @@ export class SaveMappingsComponent implements OnChanges, OnDestroy { showDataQualityResults() { console.log('open modal showing dqc results') + this._dialog.open(ResultsModalComponent, { + width: '50rem', + data: { orgId: this.orgId, dqcId: this.dqcId }, + }) } ngOnDestroy(): void { diff --git a/src/app/modules/inventory-list/list/grid/actions.component.ts b/src/app/modules/inventory-list/list/grid/actions.component.ts index 36efb584..51a6185b 100644 --- a/src/app/modules/inventory-list/list/grid/actions.component.ts +++ b/src/app/modules/inventory-list/list/grid/actions.component.ts @@ -74,7 +74,7 @@ export class ActionsComponent implements OnDestroy { this._dialog.open(MoreActionsModalComponent, { width: '40rem', autoFocus: false, - data: { viewIds: this.selectedViewIds, orgId: this.orgId }, + data: { viewIds: this.selectedViewIds, orgId: this.orgId, type: this.type }, }) } diff --git a/src/app/modules/inventory-list/list/modal/more-actions-modal.component.html b/src/app/modules/inventory-list/list/modal/more-actions-modal.component.html index 07bc471a..c15e3974 100644 --- a/src/app/modules/inventory-list/list/modal/more-actions-modal.component.html +++ b/src/app/modules/inventory-list/list/modal/more-actions-modal.component.html @@ -4,39 +4,55 @@
-
-
    - @for (item of actionsColumn1; track $index) { -
  • - -
  • - } -
-
    - @for (item of actionsColumn2; track $index) { -
  • - -
  • - } -
+ +
+
+
    + @for (item of actionsColumn1; track $index) { +
  • + +
  • + } +
+
    + @for (item of actionsColumn2; track $index) { +
  • + +
  • + } +
+
+ + + @if (showProgress) { +
+ + +
+ } +
- +
diff --git a/src/app/modules/inventory-list/list/modal/more-actions-modal.component.ts b/src/app/modules/inventory-list/list/modal/more-actions-modal.component.ts index d9622e12..42cd9533 100644 --- a/src/app/modules/inventory-list/list/modal/more-actions-modal.component.ts +++ b/src/app/modules/inventory-list/list/modal/more-actions-modal.component.ts @@ -1,29 +1,49 @@ import type { OnDestroy } from '@angular/core' import { Component, inject } from '@angular/core' import { MatButtonModule } from '@angular/material/button' -import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog' +import { MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog' import { MatDividerModule } from '@angular/material/divider' import { MatIconModule } from '@angular/material/icon' -import { Subject } from 'rxjs' +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner' +import { finalize, Subject, switchMap, take, tap } from 'rxjs' +import { DataQualityService } from '@seed/api/data-quality' +import { ProgressBarComponent } from '@seed/components' +import { UploaderService } from '@seed/services/uploader' +import { ResultsModalComponent } from 'app/modules/data-quality' +import type { InventoryType } from 'app/modules/inventory/inventory.types' @Component({ selector: 'seed-inventory-more-actions-modal', templateUrl: './more-actions-modal.component.html', - imports: [MatButtonModule, MatDialogModule, MatDividerModule, MatIconModule], + imports: [ + MatButtonModule, + MatDialogModule, + MatDividerModule, + MatIconModule, + MatProgressSpinnerModule, + ProgressBarComponent, + ResultsModalComponent, + ], }) export class MoreActionsModalComponent implements OnDestroy { + private _dataQualityService = inject(DataQualityService) + private _dialog = inject(MatDialog) + private _uploaderService = inject(UploaderService) private _dialogRef = inject(MatDialogRef) private readonly _unsubscribeAll$ = new Subject() - data = inject(MAT_DIALOG_DATA) as { viewIds: number[]; orgId: number } + data = inject(MAT_DIALOG_DATA) as { viewIds: number[]; orgId: number; type: InventoryType } errorMessage = false + progressBarObj = this._uploaderService.defaultProgressBarObj + showProgress = false + progressTitle = '' actionsColumn1 = [ { name: 'Analysis: Run', action: this.tempAction, disabled: !this.data.viewIds.length }, { name: 'Audit Template: Export', action: this.tempAction, disabled: !this.data.viewIds.length }, { name: 'Audit Template: Update', action: this.tempAction, disabled: !this.data.viewIds.length }, { name: 'Change Access Level', action: this.tempAction, disabled: !this.data.viewIds.length }, - { name: 'Data Quality Check', action: this.tempAction, disabled: !this.data.viewIds.length }, + { name: 'Data Quality Check', action: () => { this.dataQualityCheck() }, disabled: !this.data.viewIds.length }, { name: 'Delete', action: this.tempAction, disabled: !this.data.viewIds.length }, { name: 'Derived Data: Update', action: this.tempAction, disabled: !this.data.viewIds.length }, { name: 'Email', action: this.tempAction, disabled: !this.data.viewIds.length }, @@ -46,11 +66,39 @@ export class MoreActionsModalComponent implements OnDestroy { console.log('temp action') } - close() { - this._dialogRef.close() + dataQualityCheck() { + const [propertyViewIds, taxlotViewIds] = this.data.type === 'properties' ? [this.data.viewIds, []] : [[], this.data.viewIds] + this.progressBarObj.statusMessage = 'Running Data Quality Check...' + this.showProgress = true + this._dataQualityService.startDataQualityCheckForOrg(this.data.orgId, propertyViewIds, taxlotViewIds, null) + .pipe( + take(1), + switchMap(({ progress_key }) => { + return this._uploaderService.checkProgressLoop({ + progressKey: progress_key, + offset: 0, + multiplier: 1, + successFn: () => null, + failureFn: () => null, + progressBarObj: this.progressBarObj, + }) + }), + tap(({ unique_id }) => { this.openDataQualityResultsModal(unique_id) }), + finalize(() => { this.showProgress = false }), + ) + .subscribe() + } + + openDataQualityResultsModal(dqcId: number) { + this._dialog.open(ResultsModalComponent, { + width: '50rem', + data: { orgId: this.data.orgId, dqcId }, + }) + + this.close() } - dismiss() { + close() { this._dialogRef.close() } From a48db62040f105ff6e32d1c093df78f4cdfe07f6 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Tue, 8 Jul 2025 18:48:09 +0000 Subject: [PATCH 29/37] fetch and apply profile --- .../column_mapping_profile.service.ts | 9 ++++- .../column_mapping_profile.types.ts | 2 + src/@seed/api/mapping/mapping.service.ts | 3 -- .../data-mappings/data-mapping.component.html | 1 + .../data-mappings/data-mapping.component.ts | 18 +++++++-- .../step1/map-data.component.html | 34 ++++++++++++---- .../data-mappings/step1/map-data.component.ts | 39 ++++++++++++++++--- .../data-mappings/step4/results.component.ts | 1 - .../list/grid/actions.component.ts | 8 +++- .../modal/more-actions-modal.component.ts | 6 +-- 10 files changed, 95 insertions(+), 26 deletions(-) diff --git a/src/@seed/api/column_mapping_profile/column_mapping_profile.service.ts b/src/@seed/api/column_mapping_profile/column_mapping_profile.service.ts index 0d08df4d..44c66c6c 100644 --- a/src/@seed/api/column_mapping_profile/column_mapping_profile.service.ts +++ b/src/@seed/api/column_mapping_profile/column_mapping_profile.service.ts @@ -10,6 +10,7 @@ import type { ColumnMappingProfile, ColumnMappingProfileDeleteResponse, ColumnMappingProfilesRequest, + ColumnMappingProfileType, ColumnMappingProfileUpdateResponse, ColumnMappingSuggestionResponse, } from './column_mapping_profile.types' @@ -30,9 +31,13 @@ export class ColumnMappingProfileService { }) } - getProfiles(org_id: number): Observable { + getProfiles(org_id: number, columnMappingProfileTypes: ColumnMappingProfileType[] = []): Observable { const url = `/api/v3/column_mapping_profiles/filter/?organization_id=${org_id}` - return this._httpClient.post(url, {}).pipe( + const data: Record = {} + if (columnMappingProfileTypes.length) { + data.profile_type = columnMappingProfileTypes + } + return this._httpClient.post(url, data).pipe( map((response) => { this._profiles.next(response.data) return response.data diff --git a/src/@seed/api/column_mapping_profile/column_mapping_profile.types.ts b/src/@seed/api/column_mapping_profile/column_mapping_profile.types.ts index f2fd5a0b..6e28494d 100644 --- a/src/@seed/api/column_mapping_profile/column_mapping_profile.types.ts +++ b/src/@seed/api/column_mapping_profile/column_mapping_profile.types.ts @@ -33,3 +33,5 @@ export type ColumnMappingSuggestionResponse = { status: string; data: Record; } + +export type ColumnMappingProfileType = 'Normal' | 'BuildingSync Default' | 'BuildingSync Custom' diff --git a/src/@seed/api/mapping/mapping.service.ts b/src/@seed/api/mapping/mapping.service.ts index 5e09ff0c..c01edf42 100644 --- a/src/@seed/api/mapping/mapping.service.ts +++ b/src/@seed/api/mapping/mapping.service.ts @@ -94,9 +94,6 @@ export class MappingService { // returns ProgressResponse if already matched return this._httpClient.post(url, {}) .pipe( - tap((response) => { - console.log('Match merge started:', response) - }), catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error starting match merge') }), diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.html b/src/app/modules/datasets/data-mappings/data-mapping.component.html index 0016eab4..8904f597 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.html +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.html @@ -31,6 +31,7 @@ () + private _columnMappingProfileService = inject(ColumnMappingProfileService) private _columnService = inject(ColumnService) private _cycleService = inject(CycleService) private _datasetService = inject(DatasetService) @@ -72,6 +75,8 @@ export class DataMappingComponent implements OnDestroy, OnInit { private _uploaderService = inject(UploaderService) private _userService = inject(UserService) columns: Column[] + columnMappingProfiles: ColumnMappingProfile[] = [] + columnMappingProfileTypes: ColumnMappingProfileType[] columnNames: string[] completed = { 1: false, 2: false, 3: false, 4: false } currentProfile: Profile @@ -114,7 +119,10 @@ export class DataMappingComponent implements OnDestroy, OnInit { return this._datasetService.getImportFile(this.orgId, this.fileId) .pipe( take(1), - tap((importFile) => { this.importFile = importFile }), + tap((importFile) => { + this.importFile = importFile + this.columnMappingProfileTypes = importFile.source_type === 'BuildingSync Raw' ? ['BuildingSync Default', 'BuildingSync Custom'] : ['Normal'] + }), catchError(() => { return of(null) }), @@ -123,6 +131,7 @@ export class DataMappingComponent implements OnDestroy, OnInit { getMappingData() { return forkJoin([ + this._columnMappingProfileService.getProfiles(this.orgId, this.columnMappingProfileTypes), this._cycleService.getCycle(this.orgId, this.importFile.cycle), this._mappingService.firstFiveRows(this.orgId, this.fileId), this._mappingService.mappingSuggestions(this.orgId, this.fileId), @@ -130,7 +139,8 @@ export class DataMappingComponent implements OnDestroy, OnInit { ]) .pipe( take(1), - tap(([cycle, firstFiveRows, mappingSuggestions, rawColumnNames]) => { + tap(([columnMappingProfiles, cycle, firstFiveRows, mappingSuggestions, rawColumnNames]) => { + this.columnMappingProfiles = columnMappingProfiles this.cycle = cycle this.firstFiveRows = firstFiveRows this.mappingSuggestions = mappingSuggestions diff --git a/src/app/modules/datasets/data-mappings/step1/map-data.component.html b/src/app/modules/datasets/data-mappings/step1/map-data.component.html index 5b92c7f4..6b7765bc 100644 --- a/src/app/modules/datasets/data-mappings/step1/map-data.component.html +++ b/src/app/modules/datasets/data-mappings/step1/map-data.component.html @@ -1,20 +1,40 @@ - -
-
+
+
Cycle:
{{ cycle?.name }}
-
+ +
Column Profile:
-
none selected
+ + + @for (profile of columnMappingProfiles; track profile.id) { + {{ profile.name }} + } + + + + + + +
-
+
- +
diff --git a/src/app/modules/datasets/data-mappings/step1/map-data.component.ts b/src/app/modules/datasets/data-mappings/step1/map-data.component.ts index b0ec7cf2..8cc7c714 100644 --- a/src/app/modules/datasets/data-mappings/step1/map-data.component.ts +++ b/src/app/modules/datasets/data-mappings/step1/map-data.component.ts @@ -1,27 +1,29 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ import { CommonModule } from '@angular/common' import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core' -import { Component, EventEmitter, inject, Input, Output } from '@angular/core' +import { Component, EventEmitter, inject, input, Input, Output } from '@angular/core' import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { MatButtonModule } from '@angular/material/button' import { MatButtonToggleModule } from '@angular/material/button-toggle' +import { MatOptionModule } from '@angular/material/core' import { MatDividerModule } from '@angular/material/divider' import { MatIconModule } from '@angular/material/icon' import { MatSelectModule } from '@angular/material/select' import { MatSidenavModule } from '@angular/material/sidenav' import { MatStepperModule } from '@angular/material/stepper' import { ActivatedRoute } from '@angular/router' +import { AgGridAngular } from 'ag-grid-angular' +import type { CellValueChangedEvent, ColDef, GridApi, GridReadyEvent, RowNode } from 'ag-grid-community' +import { Subject } from 'rxjs' import { type Column } from '@seed/api/column' +import type { ColumnMappingProfile } from '@seed/api/column_mapping_profile' import type { Cycle } from '@seed/api/cycle' import type { DataMappingRow, ImportFile } from '@seed/api/dataset' import type { MappingSuggestionsResponse } from '@seed/api/mapping' import { AlertComponent, PageComponent } from '@seed/components' import { ConfigService } from '@seed/services' import type { ProgressBarObj } from '@seed/services/uploader' -import { AgGridAngular } from 'ag-grid-angular' -import type { CellValueChangedEvent, ColDef, GridApi, GridReadyEvent, RowNode } from 'ag-grid-community' import type { InventoryDisplayType, Profile } from 'app/modules/inventory' -import { Subject } from 'rxjs' import { HelpComponent } from '../help.component' import { buildColumnDefs, gridOptions } from './column-defs' import { dataTypeMap, displayToDataTypeMap } from './constants' @@ -38,6 +40,7 @@ import { dataTypeMap, displayToDataTypeMap } from './constants' MatButtonToggleModule, MatDividerModule, MatIconModule, + MatOptionModule, MatSidenavModule, MatSelectModule, MatStepperModule, @@ -49,6 +52,7 @@ import { dataTypeMap, displayToDataTypeMap } from './constants' export class MapDataComponent implements OnChanges, OnDestroy { @Input() orgId: number @Input() importFile: ImportFile + @Input() columnMappingProfiles: ColumnMappingProfile[] @Input() cycle: Cycle @Input() firstFiveRows: Record[] @Input() mappingSuggestions: MappingSuggestionsResponse @@ -60,6 +64,7 @@ export class MapDataComponent implements OnChanges, OnDestroy { private readonly _unsubscribeAll$ = new Subject() private _configService = inject(ConfigService) private _router = inject(ActivatedRoute) + profile: ColumnMappingProfile columns: Column[] columnNames: string[] columnMap: Record @@ -196,6 +201,29 @@ export class MapDataComponent implements OnChanges, OnDestroy { }) } + applyProfile() { + const mappingsMap = Object.fromEntries(this.profile.mappings.map((m) => [m.from_field, m])) + const columnNameMap = Object.fromEntries(this.columns.map((c) => [c.column_name, c.display_name])) + this.gridApi.forEachNode((node: RowNode<{ from_field: string }>) => { + const mapping = mappingsMap[node.data.from_field] + if (!mapping) return // skip if no mapping found + + const displayField = columnNameMap[mapping.to_field] ?? '' + node.setDataValue('to_field_display_name', displayField) + node.setDataValue('to_field', mapping.to_field) + node.setDataValue('from_units', mapping.from_units) + node.setDataValue('to_table_name', mapping.to_table_name) + }) + } + + saveProfile() { + console.log('save profile') + } + + createProfile() { + console.log('create profile') + } + // Format data for backend consumption mapData() { if (!this.dataValid) return @@ -220,7 +248,7 @@ export class MapDataComponent implements OnChanges, OnDestroy { validateData() { this.errorMessages = [] const matchingColumns = this.defaultInventoryType === 'Tax Lot' ? this.matchingTaxLotColumns : this.matchingPropertyColumns - const toFields = [] + let toFields = [] this.gridApi.forEachNode((node: RowNode) => { if (node.data.omit) return // skip omitted rows toFields.push(node.data.to_field) @@ -240,6 +268,7 @@ export class MapDataComponent implements OnChanges, OnDestroy { } // no duplicates + toFields = toFields.filter((f) => f) if (toFields.length !== new Set(toFields).size) { this.dataValid = false this.errorMessages.push('Duplicate headers found. Each SEED Header must be unique.') diff --git a/src/app/modules/datasets/data-mappings/step4/results.component.ts b/src/app/modules/datasets/data-mappings/step4/results.component.ts index 2724e94e..b3671c86 100644 --- a/src/app/modules/datasets/data-mappings/step4/results.component.ts +++ b/src/app/modules/datasets/data-mappings/step4/results.component.ts @@ -51,7 +51,6 @@ export class ResultsComponent implements OnChanges, OnDestroy { hasTaxlotData = false ngOnChanges(changes: SimpleChanges): void { - console.log('changes', changes) if (changes.inProgress.currentValue === false) { this.getMatchingResults() } diff --git a/src/app/modules/inventory-list/list/grid/actions.component.ts b/src/app/modules/inventory-list/list/grid/actions.component.ts index 51a6185b..9a6855dd 100644 --- a/src/app/modules/inventory-list/list/grid/actions.component.ts +++ b/src/app/modules/inventory-list/list/grid/actions.component.ts @@ -71,11 +71,17 @@ export class ActionsComponent implements OnDestroy { } openMoreActionsModal() { - this._dialog.open(MoreActionsModalComponent, { + const dialogRef = this._dialog.open(MoreActionsModalComponent, { width: '40rem', autoFocus: false, data: { viewIds: this.selectedViewIds, orgId: this.orgId, type: this.type }, }) + + dialogRef.afterClosed() + .pipe( + filter(Boolean), + tap(() => { this.refreshInventory.emit() }), + ).subscribe() } onAction(action: () => void, select: MatSelect) { diff --git a/src/app/modules/inventory-list/list/modal/more-actions-modal.component.ts b/src/app/modules/inventory-list/list/modal/more-actions-modal.component.ts index 42cd9533..e9606d07 100644 --- a/src/app/modules/inventory-list/list/modal/more-actions-modal.component.ts +++ b/src/app/modules/inventory-list/list/modal/more-actions-modal.component.ts @@ -95,11 +95,11 @@ export class MoreActionsModalComponent implements OnDestroy { data: { orgId: this.data.orgId, dqcId }, }) - this.close() + this.close(true) } - close() { - this._dialogRef.close() + close(refresh = false) { + this._dialogRef.close(refresh) } ngOnDestroy(): void { From aba8394dfb1686ad5f59593bb02f812bc8dcd9d1 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Tue, 8 Jul 2025 20:15:20 +0000 Subject: [PATCH 30/37] save profi;e --- .../column_mapping_profile.service.ts | 8 +++-- .../ag-grid/autocomplete.component.ts | 8 +++-- .../step1/map-data.component.html | 13 ++++--- .../data-mappings/step1/map-data.component.ts | 34 ++++++++++++++++--- .../columns/mappings/mappings.component.ts | 2 +- 5 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/@seed/api/column_mapping_profile/column_mapping_profile.service.ts b/src/@seed/api/column_mapping_profile/column_mapping_profile.service.ts index 44c66c6c..4e59d94e 100644 --- a/src/@seed/api/column_mapping_profile/column_mapping_profile.service.ts +++ b/src/@seed/api/column_mapping_profile/column_mapping_profile.service.ts @@ -2,8 +2,9 @@ import type { HttpErrorResponse } from '@angular/common/http' import { HttpClient } from '@angular/common/http' import { inject, Injectable } from '@angular/core' import type { Observable } from 'rxjs' -import { catchError, map, ReplaySubject } from 'rxjs' +import { catchError, map, ReplaySubject, tap } from 'rxjs' import { ErrorService } from '@seed/services' +import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' import { UserService } from '../user' import type { ColumnMapping, @@ -21,6 +22,7 @@ export class ColumnMappingProfileService { private _userService = inject(UserService) private _profiles = new ReplaySubject(1) private _errorService = inject(ErrorService) + private _snackBar = inject(SnackBarService) profiles$ = this._profiles.asObservable() @@ -63,10 +65,11 @@ export class ColumnMappingProfileService { update(org_id: number, profile: ColumnMappingProfile): Observable { const url = `/api/v3/column_mapping_profiles/${profile.id}/?organization_id=${org_id}` - return this._httpClient.put(url, { name: profile.name }).pipe( + return this._httpClient.put(url, profile).pipe( map((response) => { return response.data }), + tap(() => { this._snackBar.success('Profile updated successfully') }), catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error updating profile') }), @@ -88,6 +91,7 @@ export class ColumnMappingProfileService { create(org_id: number, profile: ColumnMappingProfile): Observable { const url = `/api/v3/column_mapping_profiles/?organization_id=${org_id}` return this._httpClient.post(url, { ...profile }).pipe( + tap(() => { this._snackBar.success('Profile created successfully') }), catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error creating profile') }), diff --git a/src/@seed/components/ag-grid/autocomplete.component.ts b/src/@seed/components/ag-grid/autocomplete.component.ts index d54a4e4b..1c31c081 100644 --- a/src/@seed/components/ag-grid/autocomplete.component.ts +++ b/src/@seed/components/ag-grid/autocomplete.component.ts @@ -23,12 +23,12 @@ export class AutocompleteCellComponent implements ICellEditorAngularComp, AfterV inputCtrl = new FormControl('') filteredOptions: string[] = [] - params!: unknown + params!: ICellEditorParams & { values?: string[] } options: string[] = [] - agInit(params: ICellEditorParams): void { + agInit(params: ICellEditorParams & { values?: string[] }): void { this.params = params - this.options = ((params as unknown) as { values: string[] }).values || [] + this.options = params.values || [] this.inputCtrl.setValue(params.value as string) this.filteredOptions = [...this.options] this.inputCtrl.valueChanges.subscribe((value) => { @@ -36,6 +36,8 @@ export class AutocompleteCellComponent implements ICellEditorAngularComp, AfterV this.filteredOptions = this.options.filter((option) => { return option.toLowerCase().startsWith(value.toLowerCase()) }) + // update after each keystroke + this.params.node.setDataValue(this.params.column.getId(), value) }) } diff --git a/src/app/modules/datasets/data-mappings/step1/map-data.component.html b/src/app/modules/datasets/data-mappings/step1/map-data.component.html index 6b7765bc..fd535ec0 100644 --- a/src/app/modules/datasets/data-mappings/step1/map-data.component.html +++ b/src/app/modules/datasets/data-mappings/step1/map-data.component.html @@ -7,7 +7,7 @@
-
Column Profile:
+
Column Profile:
{{ profile.name }} } - + + + + + diff --git a/src/app/modules/datasets/data-mappings/step1/map-data.component.ts b/src/app/modules/datasets/data-mappings/step1/map-data.component.ts index 8cc7c714..6856f732 100644 --- a/src/app/modules/datasets/data-mappings/step1/map-data.component.ts +++ b/src/app/modules/datasets/data-mappings/step1/map-data.component.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ import { CommonModule } from '@angular/common' import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core' -import { Component, EventEmitter, inject, input, Input, Output } from '@angular/core' +import { Component, EventEmitter, inject, Input, Output } from '@angular/core' import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { MatButtonModule } from '@angular/material/button' import { MatButtonToggleModule } from '@angular/material/button-toggle' @@ -11,12 +11,13 @@ import { MatIconModule } from '@angular/material/icon' import { MatSelectModule } from '@angular/material/select' import { MatSidenavModule } from '@angular/material/sidenav' import { MatStepperModule } from '@angular/material/stepper' +import { MatTooltipModule } from '@angular/material/tooltip' import { ActivatedRoute } from '@angular/router' import { AgGridAngular } from 'ag-grid-angular' import type { CellValueChangedEvent, ColDef, GridApi, GridReadyEvent, RowNode } from 'ag-grid-community' import { Subject } from 'rxjs' import { type Column } from '@seed/api/column' -import type { ColumnMappingProfile } from '@seed/api/column_mapping_profile' +import { type ColumnMapping, type ColumnMappingProfile, ColumnMappingProfileService } from '@seed/api/column_mapping_profile' import type { Cycle } from '@seed/api/cycle' import type { DataMappingRow, ImportFile } from '@seed/api/dataset' import type { MappingSuggestionsResponse } from '@seed/api/mapping' @@ -44,6 +45,7 @@ import { dataTypeMap, displayToDataTypeMap } from './constants' MatSidenavModule, MatSelectModule, MatStepperModule, + MatTooltipModule, PageComponent, ReactiveFormsModule, FormsModule, @@ -63,6 +65,7 @@ export class MapDataComponent implements OnChanges, OnDestroy { private readonly _unsubscribeAll$ = new Subject() private _configService = inject(ConfigService) + private _columnMappingProfileService = inject(ColumnMappingProfileService) private _router = inject(ActivatedRoute) profile: ColumnMappingProfile columns: Column[] @@ -106,6 +109,7 @@ export class MapDataComponent implements OnChanges, OnDestroy { to_field_display_name: null, to_table_name: this.defaultInventoryType, to_data_type: null, + to_field: null, from_units: null, } this.setColumnDefs() @@ -170,7 +174,6 @@ export class MapDataComponent implements OnChanges, OnDestroy { to_field, from_units: dataTypeConfig.units, }) - this.refreshNode(node) this.validateData() } @@ -202,6 +205,7 @@ export class MapDataComponent implements OnChanges, OnDestroy { } applyProfile() { + const toTableMap = { TaxLotState: 'Tax Lot', PropertyState: 'Property' } const mappingsMap = Object.fromEntries(this.profile.mappings.map((m) => [m.from_field, m])) const columnNameMap = Object.fromEntries(this.columns.map((c) => [c.column_name, c.display_name])) this.gridApi.forEachNode((node: RowNode<{ from_field: string }>) => { @@ -212,12 +216,32 @@ export class MapDataComponent implements OnChanges, OnDestroy { node.setDataValue('to_field_display_name', displayField) node.setDataValue('to_field', mapping.to_field) node.setDataValue('from_units', mapping.from_units) - node.setDataValue('to_table_name', mapping.to_table_name) + node.setDataValue('to_table_name', toTableMap[mapping.to_table_name]) }) } saveProfile() { - console.log('save profile') + // overwrite the existing profile + const mappings: ColumnMapping[] = [] + this.gridApi.forEachNode((node) => { + const mapping = this.formatRowToMapping(node.data) + if (mapping) mappings.push(mapping) + }) + this.profile.mappings = mappings + this._columnMappingProfileService.update(this.orgId, this.profile).subscribe() + } + + formatRowToMapping(row: Record): ColumnMapping { + if (!row.to_field) return null + const to_table_name = row.to_table_name === 'Tax Lot' ? 'TaxLotState' : 'PropertyState' + const mapping: ColumnMapping = { + from_field: row.from_field as string, + from_units: row.from_units as string, + to_field: row.to_field as string, + to_table_name, + } + if (row.omit) mapping.is_omitted = true + return mapping } createProfile() { diff --git a/src/app/modules/organizations/columns/mappings/mappings.component.ts b/src/app/modules/organizations/columns/mappings/mappings.component.ts index b3eedb1c..dd50c59a 100644 --- a/src/app/modules/organizations/columns/mappings/mappings.component.ts +++ b/src/app/modules/organizations/columns/mappings/mappings.component.ts @@ -107,7 +107,7 @@ export class MappingsComponent implements ComponentCanDeactivate, OnDestroy, OnI field: 'to_table_name', editable: false, valueFormatter: (params: ValueFormatterParams) => { - return (params.value as string).slice(0, -5) + return (params.value as string)?.slice(0, -5) }, }, { From 8ec5e37020aa4c58fd02a86028e093661dcd80eb Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 9 Jul 2025 13:57:02 +0000 Subject: [PATCH 31/37] profiles fxnal --- .../column_mapping_profile.service.ts | 28 +++++----- .../data-mappings/data-mapping.component.ts | 15 ++++- .../step1/map-data.component.html | 6 +- .../data-mappings/step1/map-data.component.ts | 29 ++++++++-- .../step1/modal/create-profile.component.html | 30 ++++++++++ .../step1/modal/create-profile.component.ts | 55 +++++++++++++++++++ 6 files changed, 139 insertions(+), 24 deletions(-) create mode 100644 src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.html create mode 100644 src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.ts diff --git a/src/@seed/api/column_mapping_profile/column_mapping_profile.service.ts b/src/@seed/api/column_mapping_profile/column_mapping_profile.service.ts index 4e59d94e..7d2f2ae1 100644 --- a/src/@seed/api/column_mapping_profile/column_mapping_profile.service.ts +++ b/src/@seed/api/column_mapping_profile/column_mapping_profile.service.ts @@ -33,8 +33,8 @@ export class ColumnMappingProfileService { }) } - getProfiles(org_id: number, columnMappingProfileTypes: ColumnMappingProfileType[] = []): Observable { - const url = `/api/v3/column_mapping_profiles/filter/?organization_id=${org_id}` + getProfiles(orgId: number, columnMappingProfileTypes: ColumnMappingProfileType[] = []): Observable { + const url = `/api/v3/column_mapping_profiles/filter/?organization_id=${orgId}` const data: Record = {} if (columnMappingProfileTypes.length) { data.profile_type = columnMappingProfileTypes @@ -51,8 +51,8 @@ export class ColumnMappingProfileService { ) } - updateMappings(org_id: number, profile_id: number, mappings: ColumnMapping[]): Observable { - const url = `/api/v3/column_mapping_profiles/${profile_id}/?organization_id=${org_id}` + updateMappings(orgId: number, profile_id: number, mappings: ColumnMapping[]): Observable { + const url = `/api/v3/column_mapping_profiles/${profile_id}/?organization_id=${orgId}` return this._httpClient.put(url, { mappings }).pipe( map((response) => { return response.data @@ -63,8 +63,8 @@ export class ColumnMappingProfileService { ) } - update(org_id: number, profile: ColumnMappingProfile): Observable { - const url = `/api/v3/column_mapping_profiles/${profile.id}/?organization_id=${org_id}` + update(orgId: number, profile: ColumnMappingProfile): Observable { + const url = `/api/v3/column_mapping_profiles/${profile.id}/?organization_id=${orgId}` return this._httpClient.put(url, profile).pipe( map((response) => { return response.data @@ -76,8 +76,8 @@ export class ColumnMappingProfileService { ) } - delete(org_id: number, profile_id: number): Observable { - const url = `/api/v3/column_mapping_profiles/${profile_id}/?organization_id=${org_id}` + delete(orgId: number, profile_id: number): Observable { + const url = `/api/v3/column_mapping_profiles/${profile_id}/?organization_id=${orgId}` return this._httpClient.delete(url).pipe( map((response) => { return response @@ -88,8 +88,8 @@ export class ColumnMappingProfileService { ) } - create(org_id: number, profile: ColumnMappingProfile): Observable { - const url = `/api/v3/column_mapping_profiles/?organization_id=${org_id}` + create(orgId: number, profile: ColumnMappingProfile): Observable { + const url = `/api/v3/column_mapping_profiles/?organization_id=${orgId}` return this._httpClient.post(url, { ...profile }).pipe( tap(() => { this._snackBar.success('Profile created successfully') }), catchError((error: HttpErrorResponse) => { @@ -98,8 +98,8 @@ export class ColumnMappingProfileService { ) } - export(org_id: number, profile_id: number) { - const url = `/api/v3/column_mapping_profiles/${profile_id}/csv/?organization_id=${org_id}` + export(orgId: number, profile_id: number) { + const url = `/api/v3/column_mapping_profiles/${profile_id}/csv/?organization_id=${orgId}` return this._httpClient.get(url, { responseType: 'text' }).pipe( map((response) => { return new Blob([response], { type: 'text/csv;charset: utf-8' }) @@ -110,8 +110,8 @@ export class ColumnMappingProfileService { ) } - suggestions(org_id: number, headers: string[]) { - const url = `/api/v3/column_mapping_profiles/suggestions/?organization_id=${org_id}` + suggestions(orgId: number, headers: string[]) { + const url = `/api/v3/column_mapping_profiles/suggestions/?organization_id=${orgId}` return this._httpClient.post(url, { headers }).pipe( map((response) => { return response.data diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.ts b/src/app/modules/datasets/data-mappings/data-mapping.component.ts index aaa8eb5e..832539bc 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.ts +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.ts @@ -109,6 +109,7 @@ export class DataMappingComponent implements OnDestroy, OnInit { }), switchMap(() => this.getImportFile()), filter(Boolean), + tap(() => { this.getProfiles() }), switchMap(() => this.getMappingData()), switchMap(() => this.getColumns()), ) @@ -131,7 +132,6 @@ export class DataMappingComponent implements OnDestroy, OnInit { getMappingData() { return forkJoin([ - this._columnMappingProfileService.getProfiles(this.orgId, this.columnMappingProfileTypes), this._cycleService.getCycle(this.orgId, this.importFile.cycle), this._mappingService.firstFiveRows(this.orgId, this.fileId), this._mappingService.mappingSuggestions(this.orgId, this.fileId), @@ -139,8 +139,7 @@ export class DataMappingComponent implements OnDestroy, OnInit { ]) .pipe( take(1), - tap(([columnMappingProfiles, cycle, firstFiveRows, mappingSuggestions, rawColumnNames]) => { - this.columnMappingProfiles = columnMappingProfiles + tap(([cycle, firstFiveRows, mappingSuggestions, rawColumnNames]) => { this.cycle = cycle this.firstFiveRows = firstFiveRows this.mappingSuggestions = mappingSuggestions @@ -172,6 +171,16 @@ export class DataMappingComponent implements OnDestroy, OnInit { ) } + getProfiles() { + this._columnMappingProfileService.getProfiles(this.orgId, this.columnMappingProfileTypes) + .pipe( + switchMap(() => this._columnMappingProfileService.profiles$), + takeUntil(this._unsubscribeAll$), + tap((profiles) => { this.columnMappingProfiles = profiles }), + ) + .subscribe() + } + onCompleted(step: number) { this.completed[step] = true this.stepper.next() diff --git a/src/app/modules/datasets/data-mappings/step1/map-data.component.html b/src/app/modules/datasets/data-mappings/step1/map-data.component.html index fd535ec0..9a80f341 100644 --- a/src/app/modules/datasets/data-mappings/step1/map-data.component.html +++ b/src/app/modules/datasets/data-mappings/step1/map-data.component.html @@ -19,13 +19,13 @@ } - + - + - +
diff --git a/src/app/modules/datasets/data-mappings/step1/map-data.component.ts b/src/app/modules/datasets/data-mappings/step1/map-data.component.ts index 6856f732..d07b5607 100644 --- a/src/app/modules/datasets/data-mappings/step1/map-data.component.ts +++ b/src/app/modules/datasets/data-mappings/step1/map-data.component.ts @@ -6,6 +6,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { MatButtonModule } from '@angular/material/button' import { MatButtonToggleModule } from '@angular/material/button-toggle' import { MatOptionModule } from '@angular/material/core' +import { MatDialog } from '@angular/material/dialog' import { MatDividerModule } from '@angular/material/divider' import { MatIconModule } from '@angular/material/icon' import { MatSelectModule } from '@angular/material/select' @@ -15,8 +16,9 @@ import { MatTooltipModule } from '@angular/material/tooltip' import { ActivatedRoute } from '@angular/router' import { AgGridAngular } from 'ag-grid-angular' import type { CellValueChangedEvent, ColDef, GridApi, GridReadyEvent, RowNode } from 'ag-grid-community' -import { Subject } from 'rxjs' +import { Subject, switchMap, take } from 'rxjs' import { type Column } from '@seed/api/column' +import type { ColumnMappingProfileType } from '@seed/api/column_mapping_profile' import { type ColumnMapping, type ColumnMappingProfile, ColumnMappingProfileService } from '@seed/api/column_mapping_profile' import type { Cycle } from '@seed/api/cycle' import type { DataMappingRow, ImportFile } from '@seed/api/dataset' @@ -24,10 +26,11 @@ import type { MappingSuggestionsResponse } from '@seed/api/mapping' import { AlertComponent, PageComponent } from '@seed/components' import { ConfigService } from '@seed/services' import type { ProgressBarObj } from '@seed/services/uploader' -import type { InventoryDisplayType, Profile } from 'app/modules/inventory' +import type { InventoryDisplayType } from 'app/modules/inventory' import { HelpComponent } from '../help.component' import { buildColumnDefs, gridOptions } from './column-defs' import { dataTypeMap, displayToDataTypeMap } from './constants' +import { CreateProfileComponent } from './modal/create-profile.component' @Component({ selector: 'seed-map-data', @@ -66,13 +69,13 @@ export class MapDataComponent implements OnChanges, OnDestroy { private readonly _unsubscribeAll$ = new Subject() private _configService = inject(ConfigService) private _columnMappingProfileService = inject(ColumnMappingProfileService) + private _dialog = inject(MatDialog) private _router = inject(ActivatedRoute) profile: ColumnMappingProfile columns: Column[] columnNames: string[] columnMap: Record columnDefs: ColDef[] - currentProfile: Profile dataValid = false defaultInventoryType: InventoryDisplayType = 'Property' defaultRow: Record @@ -212,7 +215,7 @@ export class MapDataComponent implements OnChanges, OnDestroy { const mapping = mappingsMap[node.data.from_field] if (!mapping) return // skip if no mapping found - const displayField = columnNameMap[mapping.to_field] ?? '' + const displayField = columnNameMap[mapping.to_field] ?? mapping.to_field node.setDataValue('to_field_display_name', displayField) node.setDataValue('to_field', mapping.to_field) node.setDataValue('from_units', mapping.from_units) @@ -246,6 +249,24 @@ export class MapDataComponent implements OnChanges, OnDestroy { createProfile() { console.log('create profile') + const profileType: ColumnMappingProfileType = this.importFile.source_type === 'BuildingSync' ? 'BuildingSync Custom' : 'Normal' + const profileTypes: ColumnMappingProfileType[] = profileType === 'BuildingSync Custom' ? ['BuildingSync Default', 'BuildingSync Custom'] : ['Normal'] + const dialogRef = this._dialog.open(CreateProfileComponent, { + width: '40rem', + data: { + existingNames: this.columnMappingProfiles.map((p) => p.name), + orgId: this.orgId, + profileType, + }, + }) + + dialogRef + .afterClosed() + .pipe( + take(1), + switchMap(() => this._columnMappingProfileService.getProfiles(this.orgId, profileTypes)), + ) + .subscribe() } // Format data for backend consumption diff --git a/src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.html b/src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.html new file mode 100644 index 00000000..3e8e3600 --- /dev/null +++ b/src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.html @@ -0,0 +1,30 @@ +
+ +
Create Column Mapping Profile
+
+ + + +
+ + + + Name + + @if (form.controls.name?.hasError('valueExists')) { + This name already exists. + } + + + +
+ + +
+ + + + + + +
\ No newline at end of file diff --git a/src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.ts b/src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.ts new file mode 100644 index 00000000..9aec2747 --- /dev/null +++ b/src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.ts @@ -0,0 +1,55 @@ +import { Component, inject } from '@angular/core' +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms' +import { MatButtonModule } from '@angular/material/button' +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog' +import { MatDividerModule } from '@angular/material/divider' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatIconModule } from '@angular/material/icon' +import { MatInputModule } from '@angular/material/input' +import type { ColumnMappingProfile, ColumnMappingProfileType } from '@seed/api/column_mapping_profile' +import { ColumnMappingProfileService } from '@seed/api/column_mapping_profile' +import { SEEDValidators } from '@seed/validators' + +@Component({ + selector: 'seed-data-mapping-create-profile', + templateUrl: './create-profile.component.html', + imports: [ + FormsModule, + MatIconModule, + MatButtonModule, + MatDividerModule, + MatFormFieldModule, + MatInputModule, + MatDialogModule, + ReactiveFormsModule, + ], +}) +export class CreateProfileComponent { + private _columnMappingProfileService = inject(ColumnMappingProfileService) + private _dialogRef = inject(MatDialogRef) + + profileName = '' + profile: ColumnMappingProfile + + data = inject(MAT_DIALOG_DATA) as { orgId: number; profileType: ColumnMappingProfileType; existingNames: string[] } + form = new FormGroup({ + name: new FormControl('', [Validators.required, SEEDValidators.uniqueValue(this.data.existingNames)]), + }) + + onSubmit() { + this.profile = { + name: this.form.value.name, + profile_type: this.data.profileType, + mappings: [], + } as ColumnMappingProfile + + this._columnMappingProfileService.create(this.data.orgId, this.profile) + .subscribe(() => { + this.close() + }) + } + + close() { + this._dialogRef.close() + } +} From f191f38e7722110968106835aa4825db4069ae82 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 9 Jul 2025 15:04:24 +0000 Subject: [PATCH 32/37] hook up delete taxlots from inv list --- src/@seed/api/inventory/inventory.service.ts | 14 ++++++++++++++ .../inventory-list/list/grid/actions.component.ts | 13 +++++++++---- .../inventory-list/list/inventory.component.ts | 4 +++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/@seed/api/inventory/inventory.service.ts b/src/@seed/api/inventory/inventory.service.ts index 01d3d42c..0ebb4d69 100644 --- a/src/@seed/api/inventory/inventory.service.ts +++ b/src/@seed/api/inventory/inventory.service.ts @@ -162,6 +162,20 @@ export class InventoryService { ) } + deleteTaxlotStates({ orgId, viewIds }: DeleteParams): Observable { + const url = '/api/v3/taxlots/batch_delete/' + const data = { taxlot_view_ids: viewIds } + const options = { params: { organization_id: orgId }, body: data } + return this._httpClient.delete(url, options).pipe( + tap(() => { + this._snackBar.success('Tax lot states deleted') + }), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error deleting tax lot states') + }), + ) + } + /* * Get PropertyView or TaxLotView */ diff --git a/src/app/modules/inventory-list/list/grid/actions.component.ts b/src/app/modules/inventory-list/list/grid/actions.component.ts index 9a6855dd..cd4bbbf8 100644 --- a/src/app/modules/inventory-list/list/grid/actions.component.ts +++ b/src/app/modules/inventory-list/list/grid/actions.component.ts @@ -54,7 +54,7 @@ export class ActionsComponent implements OnDestroy { }, disabled: !this.inventory, }, - { name: 'Delete', action: this.deletePropertyStates, disabled: !this.selectedViewIds.length }, + { name: 'Delete', action: this.deleteStates, disabled: !this.selectedViewIds.length }, { name: 'Merge', action: this.tempAction, disabled: !this.selectedViewIds.length }, { name: 'More...', @@ -110,10 +110,11 @@ export class ActionsComponent implements OnDestroy { this.gridApi.deselectAll() } - deletePropertyStates = () => { + deleteStates = () => { + const displayType = this.type === 'taxlots' ? 'Tax Lot' : 'Property' const dialogRef = this._dialog.open(DeleteModalComponent, { width: '40rem', - data: { model: `${this.selectedViewIds.length} Property States`, instance: '' }, + data: { model: `${this.selectedViewIds.length} ${displayType} States`, instance: '' }, }) dialogRef @@ -121,7 +122,11 @@ export class ActionsComponent implements OnDestroy { .pipe( takeUntil(this._unsubscribeAll$), filter(Boolean), - switchMap(() => this._inventoryService.deletePropertyStates({ orgId: this.orgId, viewIds: this.selectedViewIds })), + switchMap(() => { + return this.type === 'taxlots' + ? this._inventoryService.deleteTaxlotStates({ orgId: this.orgId, viewIds: this.selectedViewIds }) + : this._inventoryService.deletePropertyStates({ orgId: this.orgId, viewIds: this.selectedViewIds }) + }), tap(() => { this.refreshInventory.emit() }), diff --git a/src/app/modules/inventory-list/list/inventory.component.ts b/src/app/modules/inventory-list/list/inventory.component.ts index f84648ce..5cedc297 100644 --- a/src/app/modules/inventory-list/list/inventory.component.ts +++ b/src/app/modules/inventory-list/list/inventory.component.ts @@ -268,7 +268,9 @@ export class InventoryComponent implements OnDestroy, OnInit { } onSelectionChanged() { - this.selectedViewIds = this.gridApi.getSelectedRows().map(({ property_view_id }: { property_view_id: number }) => property_view_id) + this.selectedViewIds = this.type === 'taxlots' + ? this.gridApi.getSelectedRows().map(({ taxlot_view_id }: { taxlot_view_id: number }) => taxlot_view_id) + : this.gridApi.getSelectedRows().map(({ property_view_id }: { property_view_id: number }) => property_view_id) } onSelectAll(selectedViewIds: number[]) { From 3c2222090903763f4b6d52dac3e4362e7bbd5b36 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 9 Jul 2025 15:20:01 +0000 Subject: [PATCH 33/37] upload props and taxlots related --- .../data-mappings/step1/column-defs.ts | 18 ++++++-- .../data-mappings/step1/map-data.component.ts | 45 +++++++++++++------ .../step4/results.component.html | 36 ++++++++------- .../data-mappings/step4/results.component.ts | 10 ----- 4 files changed, 65 insertions(+), 44 deletions(-) diff --git a/src/app/modules/datasets/data-mappings/step1/column-defs.ts b/src/app/modules/datasets/data-mappings/step1/column-defs.ts index e1f01159..a6c07089 100644 --- a/src/app/modules/datasets/data-mappings/step1/column-defs.ts +++ b/src/app/modules/datasets/data-mappings/step1/column-defs.ts @@ -39,8 +39,18 @@ const dropdownRenderer = (params: ICellRendererParams) => { const canEditClass = 'bg-primary bg-opacity-25 rounded' +const getColumnOptions = (params: ICellRendererParams, propertyColumnNames: string[], taxlotColumnNames: string[]) => { + const data = params.data as { to_table_name: 'Property' | 'Tax Lot' } + const to_table_name = data.to_table_name + if (to_table_name === 'Tax Lot') { + return taxlotColumnNames + } + return propertyColumnNames +} + export const buildColumnDefs = ( - columnNames: string[], + propertyColumnNames: string[], + taxlotColumnNames: string[], uploadedFilename: string, seedHeaderChange: (event: CellValueChangedEvent) => void, dataTypeChange: (event: CellValueChangedEvent) => void, @@ -77,9 +87,9 @@ export const buildColumnDefs = ( field: 'to_field_display_name', headerName: 'SEED Header', cellEditor: AutocompleteCellComponent, - cellEditorParams: { - values: columnNames, - }, + cellEditorParams: (params) => { + return { values: getColumnOptions(params, propertyColumnNames, taxlotColumnNames)} + } , headerComponent: EditHeaderComponent, headerComponentParams: { name: 'SEED Header', diff --git a/src/app/modules/datasets/data-mappings/step1/map-data.component.ts b/src/app/modules/datasets/data-mappings/step1/map-data.component.ts index d07b5607..e5ec3149 100644 --- a/src/app/modules/datasets/data-mappings/step1/map-data.component.ts +++ b/src/app/modules/datasets/data-mappings/step1/map-data.component.ts @@ -71,10 +71,6 @@ export class MapDataComponent implements OnChanges, OnDestroy { private _columnMappingProfileService = inject(ColumnMappingProfileService) private _dialog = inject(MatDialog) private _router = inject(ActivatedRoute) - profile: ColumnMappingProfile - columns: Column[] - columnNames: string[] - columnMap: Record columnDefs: ColDef[] dataValid = false defaultInventoryType: InventoryDisplayType = 'Property' @@ -85,7 +81,14 @@ export class MapDataComponent implements OnChanges, OnDestroy { gridOptions = gridOptions gridTheme$ = this._configService.gridTheme$ mappedData: { mappings: DataMappingRow[] } = { mappings: [] } + profile: ColumnMappingProfile + propertyColumns: Column[] = [] + propertyColumnMap: Record + propertyColumnNames: string[] rowData: Record[] = [] + taxlotColumns: Column[] = [] + taxlotColumnMap: Record + taxlotColumnNames: string[] progressBarObj: ProgressBarObj = { message: [], @@ -121,7 +124,8 @@ export class MapDataComponent implements OnChanges, OnDestroy { setColumnDefs() { this.columnDefs = buildColumnDefs( - this.columnNames, + this.propertyColumnNames, + this.taxlotColumnNames, this.importFile.uploaded_filename, this.seedHeaderChange.bind(this), this.dataTypeChange.bind(this), @@ -152,20 +156,23 @@ export class MapDataComponent implements OnChanges, OnDestroy { setAllInventoryType(value: InventoryDisplayType) { this.defaultInventoryType = value - this.gridApi.forEachNode((node) => node.setDataValue('to_table_display_name', value)) + this.gridApi.forEachNode((node) => node.setDataValue('to_table_name', value)) this.setColumns() } setColumns() { - this.columns = this.defaultInventoryType === 'Tax Lot' ? this.mappingSuggestions?.taxlot_columns : this.mappingSuggestions?.property_columns - this.columnNames = this.columns.map((c) => c.display_name) - this.columnMap = Object.fromEntries(this.columns.map((c) => [c.display_name, c])) + this.propertyColumns = this.mappingSuggestions?.property_columns ?? [] + this.propertyColumnNames = this.mappingSuggestions?.property_columns.map((c) => c.display_name) ?? [] + this.propertyColumnMap = Object.fromEntries(this.propertyColumns.map((c) => [c.display_name, c])) + this.taxlotColumns = this.mappingSuggestions?.taxlot_columns ?? [] + this.taxlotColumnNames = this.mappingSuggestions?.taxlot_columns.map((c) => c.display_name) ?? [] + this.taxlotColumnMap = Object.fromEntries(this.taxlotColumns.map((c) => [c.display_name, c])) } seedHeaderChange = (params: CellValueChangedEvent): void => { const node = params.node as RowNode const newValue = params.newValue as string - const column = this.columnMap[newValue] ?? null + const column = this.getColumnMap(node.data)[newValue] ?? null const dataTypeConfig = dataTypeMap[column?.data_type] ?? { display: 'None', units: null } const to_field = column?.column_name ?? newValue @@ -181,6 +188,16 @@ export class MapDataComponent implements OnChanges, OnDestroy { this.validateData() } + getColumnMap(nodeData: { to_table_name: InventoryDisplayType }) { + if (nodeData.to_table_name === 'Tax Lot') return this.taxlotColumnMap + if (nodeData.to_table_name === 'Property') return this.propertyColumnMap + return this.defaultInventoryType === 'Tax Lot' ? this.taxlotColumnMap : this.propertyColumnMap + } + + getColumns() { + return this.defaultInventoryType === 'Tax Lot' ? this.taxlotColumns : this.propertyColumns + } + dataTypeChange = (params: CellValueChangedEvent): void => { const node = params.node as RowNode node.setDataValue('from_units', null) @@ -195,8 +212,8 @@ export class MapDataComponent implements OnChanges, OnDestroy { } copyHeadersToSeed() { - const { property_columns, taxlot_columns, suggested_column_mappings } = this.mappingSuggestions - const columns = this.defaultInventoryType === 'Tax Lot' ? taxlot_columns : property_columns + const { suggested_column_mappings } = this.mappingSuggestions + const columns = this.getColumns() const columnMap: Record = columns.reduce((acc, { column_name, display_name }) => ({ ...acc, [column_name]: display_name }), {}) this.gridApi.forEachNode((node: RowNode<{ from_field: string }>) => { @@ -210,7 +227,7 @@ export class MapDataComponent implements OnChanges, OnDestroy { applyProfile() { const toTableMap = { TaxLotState: 'Tax Lot', PropertyState: 'Property' } const mappingsMap = Object.fromEntries(this.profile.mappings.map((m) => [m.from_field, m])) - const columnNameMap = Object.fromEntries(this.columns.map((c) => [c.column_name, c.display_name])) + const columnNameMap = Object.fromEntries(this.getColumns().map((c) => [c.column_name, c.display_name])) this.gridApi.forEachNode((node: RowNode<{ from_field: string }>) => { const mapping = mappingsMap[node.data.from_field] if (!mapping) return // skip if no mapping found @@ -302,7 +319,7 @@ export class MapDataComponent implements OnChanges, OnDestroy { // at least one matching column const hasMatchingCol = toFields.some((col) => matchingColumns.includes(col)) if (!hasMatchingCol) { - const matchingColNames = this.columns.filter((c) => c.is_matching_criteria).map((c) => c.display_name).join(', ') + const matchingColNames = this.getColumns().filter((c) => c.is_matching_criteria).map((c) => c.display_name).join(', ') this.errorMessages.push(`At least one of the following Property fields is required: ${matchingColNames}.`) } diff --git a/src/app/modules/datasets/data-mappings/step4/results.component.html b/src/app/modules/datasets/data-mappings/step4/results.component.html index 8cc9c5e9..a5e2f3dd 100644 --- a/src/app/modules/datasets/data-mappings/step4/results.component.html +++ b/src/app/modules/datasets/data-mappings/step4/results.component.html @@ -28,37 +28,41 @@
-
+
Properties
- + [rowData]="propertyData" + [columnDefs]="inventoryColDefs" + [theme]="gridTheme$ | async" + [domLayout]="'autoHeight'" + [style.width.px]="400" + > +
} @if (hasTaxlotData) { - +
+ +
+
-
+
Tax Lots
- + [rowData]="taxlotData" + [columnDefs]="inventoryColDefs" + [theme]="gridTheme$ | async" + [domLayout]="'autoHeight'" + [style.width.px]="400" + > +
} diff --git a/src/app/modules/datasets/data-mappings/step4/results.component.ts b/src/app/modules/datasets/data-mappings/step4/results.component.ts index b3671c86..bf8f81af 100644 --- a/src/app/modules/datasets/data-mappings/step4/results.component.ts +++ b/src/app/modules/datasets/data-mappings/step4/results.component.ts @@ -67,19 +67,9 @@ export class ResultsComponent implements OnChanges, OnDestroy { setGrid(results: MatchingResultsResponse) { this.matchingResults = results - this.setGeneralGrid() this.setInventoryGrids() } - setGeneralGrid() { - this.generalColDefs = [ - { field: 'import_file_records', headerName: 'Records in File' }, - { field: 'multiple_cycle_upload', headerName: 'Multi Cycle Upload' }, - ] - const { import_file_records, multiple_cycle_upload } = this.matchingResults - this.generalData = [{ import_file_records, multiple_cycle_upload }] - } - setInventoryGrids() { this.setPropertyData() this.setTaxLotData() From 0c974dcf1527018014319a74d3ec3c93b1702f44 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 9 Jul 2025 15:59:28 +0000 Subject: [PATCH 34/37] show both taxlot and property results --- .../data-mappings/data-mapping.component.html | 1 - .../data-mappings/data-mapping.component.ts | 4 -- .../step3/save-mappings.component.html | 35 +++++++++--- .../step3/save-mappings.component.ts | 57 ++++++++----------- .../step4/results.component.html | 2 +- 5 files changed, 53 insertions(+), 46 deletions(-) diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.html b/src/app/modules/datasets/data-mappings/data-mapping.component.html index 8904f597..fcc479d3 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.html +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.html @@ -61,7 +61,6 @@ [org]="org" [orgId]="orgId" (completed)="startMatchMerge()" - (inventoryTypeChange)="onInventoryTypeChange($event)" > diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.ts b/src/app/modules/datasets/data-mappings/data-mapping.component.ts index 832539bc..b7b814ad 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.ts +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.ts @@ -250,10 +250,6 @@ export class DataMappingComponent implements OnDestroy, OnInit { }) } - onInventoryTypeChange(inventoryType: InventoryType) { - this.inventoryType = inventoryType - } - toggleHelp = () => { this.helpOpened = !this.helpOpened } diff --git a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html index a5f07b0c..39e866c9 100644 --- a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html +++ b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html @@ -23,25 +23,46 @@ class="mb-2" (click)="saveData()" color="primary" - [disabled]="!rowData.length" + [disabled]="loading" mat-raised-button - [ngClass]="{ 'animate-pulse bg-gray-500': !rowData.length }" + [ngClass]="{ 'animate-pulse bg-gray-500': loading }" >Save Data
-@if (rowData.length) { +@if (propertyResults.length) { +
+ + Properties +
-} @else { + +} +@if (taxlotResults.length) { +
+ + Tax Lots +
+ + +} + +@if (loading) {
diff --git a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts index 82d4c444..b89ca3f2 100644 --- a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts +++ b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts @@ -1,11 +1,14 @@ import { CommonModule } from '@angular/common' -import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; +import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core' import { Component, EventEmitter, inject, Input, Output } from '@angular/core' import { MatButtonModule } from '@angular/material/button' import { MatDialog } from '@angular/material/dialog'; import { MatDividerModule } from '@angular/material/divider' import { MatIconModule } from '@angular/material/icon' import { MatProgressBarModule } from '@angular/material/progress-bar' +import { AgGridAngular } from 'ag-grid-angular' +import type { ColDef } from 'ag-grid-community' +import { Subject, switchMap, take, tap } from 'rxjs' import type { Column } from '@seed/api/column' import type { Cycle } from '@seed/api/cycle' import { DataQualityService } from '@seed/api/data-quality' @@ -13,11 +16,8 @@ import type { ImportFile, MappingResultsResponse } from '@seed/api/dataset' import type { Organization } from '@seed/api/organization' import { ConfigService } from '@seed/services' import { UploaderService } from '@seed/services/uploader' -import { AgGridAngular } from 'ag-grid-angular' -import type { ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' -import { ResultsModalComponent } from 'app/modules/data-quality'; +import { ResultsModalComponent } from 'app/modules/data-quality' import type { InventoryType } from 'app/modules/inventory' -import { Subject, switchMap, take, tap } from 'rxjs' @Component({ selector: 'seed-save-mappings', @@ -39,35 +39,31 @@ export class SaveMappingsComponent implements OnChanges, OnDestroy { @Input() org: Organization @Input() orgId: number @Output() completed = new EventEmitter() - @Output() inventoryTypeChange = new EventEmitter() private _configService = inject(ConfigService) private _dataQualityService = inject(DataQualityService) private _dialog = inject(MatDialog) private _uploaderService = inject(UploaderService) private _unsubscribeAll$ = new Subject() - columnDefs: ColDef[] = [] + propertyDefs: ColDef[] = [] + taxlotDefs: ColDef[] = [] rowData: Record[] = [] - gridApi: GridApi gridTheme$ = this._configService.gridTheme$ - mappingResults: Record[] = [] + propertyResults: Record[] = [] + taxlotResults: Record[] = [] dqcComplete = false dqcId: number inventoryType: InventoryType + loading = true progressBarObj = this._uploaderService.defaultProgressBarObj ngOnChanges(changes: SimpleChanges): void { if (!changes.mappingResultsResponse?.currentValue) return - const { properties, tax_lots } = this.mappingResultsResponse - if (tax_lots.length) { - this.mappingResults = tax_lots - this.inventoryTypeChange.emit('taxlots') - } else { - this.mappingResults = properties - this.inventoryTypeChange.emit('properties') - } + this.loading = false + this.propertyResults = this.mappingResultsResponse.properties + this.taxlotResults = this.mappingResultsResponse.tax_lots this.startDQC() this.setGrid() @@ -99,14 +95,19 @@ export class SaveMappingsComponent implements OnChanges, OnDestroy { } setGrid() { - this.setColumnDefs() - this.setRowData() + if (this.propertyResults.length) { + const propertyKeys = Object.keys(this.propertyResults[0] ?? {}) + this.propertyDefs = this.setColumnDefs(propertyKeys) + } + if (this.taxlotResults.length) { + const taxlotKeys = Object.keys(this.taxlotResults[0] ?? {}) + this.taxlotDefs = this.setColumnDefs(taxlotKeys) + } } - setColumnDefs() { + setColumnDefs(keys: string[]): ColDef[] { const aliClass = 'bg-primary bg-opacity-25' - let keys = Object.keys(this.mappingResults[0] ?? {}) // remove ALI & hidden cols const excludeKeys = ['id', 'lot_number', 'raw_access_level_instance_error', ...this.org.access_level_names] keys = keys.filter((k) => !excludeKeys.includes(k)) @@ -125,25 +126,15 @@ export class SaveMappingsComponent implements OnChanges, OnDestroy { const columnNameMap: Record = this.columns.reduce((acc, { name, display_name }) => ({ ...acc, [name]: display_name }), {}) const inventoryColumnDefs = keys.map((key) => ({ field: key, headerName: columnNameMap[key] || key })) - this.columnDefs = [...hiddenColumnDefs, ...aliColumnDefs, ...inventoryColumnDefs] - } - - setRowData() { - this.rowData = this.mappingResults - } - - onGridReady(agGrid: GridReadyEvent) { - this.gridApi = agGrid.api + const columnDefs = [...hiddenColumnDefs, ...aliColumnDefs, ...inventoryColumnDefs] + return columnDefs } saveData() { - console.log('Saving data...') - // console.log(this.mappingResults) this.completed.emit() } showDataQualityResults() { - console.log('open modal showing dqc results') this._dialog.open(ResultsModalComponent, { width: '50rem', data: { orgId: this.orgId, dqcId: this.dqcId }, diff --git a/src/app/modules/datasets/data-mappings/step4/results.component.html b/src/app/modules/datasets/data-mappings/step4/results.component.html index a5e2f3dd..496c5133 100644 --- a/src/app/modules/datasets/data-mappings/step4/results.component.html +++ b/src/app/modules/datasets/data-mappings/step4/results.component.html @@ -45,7 +45,7 @@ } @if (hasTaxlotData) { -
+
From 327e632ef3f8552e4c49df1fd7995a48273c449a Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 9 Jul 2025 19:22:15 +0000 Subject: [PATCH 35/37] several bug fixes, features, styles --- src/@seed/api/mapping/mapping.service.ts | 3 - src/@seed/components/page/page.component.html | 2 +- .../data-mappings/data-mapping.component.html | 9 ++- .../data-mappings/data-mapping.component.ts | 12 +++- .../data-mappings/step1/column-defs.ts | 2 + .../step1/map-data.component.html | 72 ++++++++++--------- .../data-mappings/step1/map-data.component.ts | 13 +++- .../step1/modal/create-profile.component.ts | 6 +- .../step3/save-mappings.component.html | 34 +++++---- .../step3/save-mappings.component.ts | 1 + .../step4/match-merge.component.ts | 3 +- .../step4/results.component.html | 7 +- .../data-upload-modal.component.ts | 11 +-- .../property-taxlot-upload.component.html | 4 +- .../property-taxlot-upload.component.ts | 27 +++++-- .../datasets/dataset/dataset.component.ts | 3 - .../modules/datasets/datasets.component.ts | 24 ++++--- .../list/grid/grid.component.ts | 11 +-- 18 files changed, 149 insertions(+), 95 deletions(-) diff --git a/src/@seed/api/mapping/mapping.service.ts b/src/@seed/api/mapping/mapping.service.ts index c01edf42..01862c9c 100644 --- a/src/@seed/api/mapping/mapping.service.ts +++ b/src/@seed/api/mapping/mapping.service.ts @@ -70,9 +70,6 @@ export class MappingService { const url = `/api/v3/import_files/${importFileId}/mapping_results/?organization_id=${orgId}` return this._httpClient.post(url, {}) .pipe( - tap((response) => { - console.log(response) - }), catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error fetching mapping results') }), diff --git a/src/@seed/components/page/page.component.html b/src/@seed/components/page/page.component.html index c7f8c40b..3142e1b7 100644 --- a/src/@seed/components/page/page.component.html +++ b/src/@seed/components/page/page.component.html @@ -80,7 +80,7 @@

-
+
diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.html b/src/app/modules/datasets/data-mappings/data-mapping.component.html index fcc479d3..82b2a9d6 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.html +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.html @@ -29,7 +29,12 @@ - + + + check + + + @@ -61,6 +67,7 @@ [org]="org" [orgId]="orgId" (completed)="startMatchMerge()" + (backToMapping)="backToMapping()" > diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.ts b/src/app/modules/datasets/data-mappings/data-mapping.component.ts index b7b814ad..c8618c1c 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.ts +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.ts @@ -30,7 +30,7 @@ import { UserService } from '@seed/api/user' import { PageComponent, ProgressBarComponent } from '@seed/components' import { UploaderService } from '@seed/services/uploader' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' -import type { InventoryType, Profile } from 'app/modules/inventory' +import type { InventoryDisplayType, InventoryType, Profile } from 'app/modules/inventory' import { HelpComponent } from './help.component' import { MapDataComponent } from './step1/map-data.component' import { SaveMappingsComponent } from './step3/save-mappings.component' @@ -241,6 +241,7 @@ export class DataMappingComponent implements OnDestroy, OnInit { startMatchMerge() { this.nextStep(3) this.matchMergeComponent.startMatchMerge() + this.completed[4] = true } nextStep(currentStep: number) { @@ -250,10 +251,19 @@ export class DataMappingComponent implements OnDestroy, OnInit { }) } + backToMapping() { + this.stepper.selectedIndex = 0 + this.completed = { 1: false, 2: false, 3: false, 4: false } + } + toggleHelp = () => { this.helpOpened = !this.helpOpened } + onDefaultInventoryTypeChange(value: InventoryDisplayType) { + this.inventoryType = value === 'Tax Lot' ? 'taxlots' : 'properties' + } + ngOnDestroy(): void { this._unsubscribeAll$.next() this._unsubscribeAll$.complete() diff --git a/src/app/modules/datasets/data-mappings/step1/column-defs.ts b/src/app/modules/datasets/data-mappings/step1/column-defs.ts index a6c07089..b39218ec 100644 --- a/src/app/modules/datasets/data-mappings/step1/column-defs.ts +++ b/src/app/modules/datasets/data-mappings/step1/column-defs.ts @@ -54,6 +54,7 @@ export const buildColumnDefs = ( uploadedFilename: string, seedHeaderChange: (event: CellValueChangedEvent) => void, dataTypeChange: (event: CellValueChangedEvent) => void, + validateData: (event?: CellValueChangedEvent) => void, ): (ColDef | ColGroupDef)[] => { const seedCols: ColDef[] = [ { field: 'isExtraData', hide: true }, @@ -66,6 +67,7 @@ export const buildColumnDefs = ( cellEditor: 'agCheckboxCellEditor', editable: true, width: 70, + onCellValueChanged: validateData, }, { field: 'to_table_name', diff --git a/src/app/modules/datasets/data-mappings/step1/map-data.component.html b/src/app/modules/datasets/data-mappings/step1/map-data.component.html index 9a80f341..749a006e 100644 --- a/src/app/modules/datasets/data-mappings/step1/map-data.component.html +++ b/src/app/modules/datasets/data-mappings/step1/map-data.component.html @@ -1,46 +1,50 @@ -
+
+ -
-
Cycle:
-
{{ cycle?.name }}
-
- - -
-
Column Profile:
- - - @for (profile of columnMappingProfiles; track profile.id) { - {{ profile.name }} - } - - - - - - - - - - - -
+ +
+
Cycle:
+
{{ cycle?.name }}
+
+
+ + +
+
Column Profile:
+ + + @for (profile of columnMappingProfiles; track profile.id) { + {{ profile.name }} + } + + + + + + + + + + + +
-
- +
- Property Types - Tax Lot Types + Properties + Tax Lots + +
diff --git a/src/app/modules/datasets/data-mappings/step1/map-data.component.ts b/src/app/modules/datasets/data-mappings/step1/map-data.component.ts index e5ec3149..2b6aa226 100644 --- a/src/app/modules/datasets/data-mappings/step1/map-data.component.ts +++ b/src/app/modules/datasets/data-mappings/step1/map-data.component.ts @@ -65,6 +65,7 @@ export class MapDataComponent implements OnChanges, OnDestroy { @Input() matchingPropertyColumns: string[] @Input() matchingTaxLotColumns: string[] @Output() completed = new EventEmitter() + @Output() defaultInventoryTypeChange = new EventEmitter() private readonly _unsubscribeAll$ = new Subject() private _configService = inject(ConfigService) @@ -129,6 +130,7 @@ export class MapDataComponent implements OnChanges, OnDestroy { this.importFile.uploaded_filename, this.seedHeaderChange.bind(this), this.dataTypeChange.bind(this), + this.validateData.bind(this), ) } @@ -156,6 +158,7 @@ export class MapDataComponent implements OnChanges, OnDestroy { setAllInventoryType(value: InventoryDisplayType) { this.defaultInventoryType = value + this.defaultInventoryTypeChange.emit(value) this.gridApi.forEachNode((node) => node.setDataValue('to_table_name', value)) this.setColumns() } @@ -242,13 +245,17 @@ export class MapDataComponent implements OnChanges, OnDestroy { saveProfile() { // overwrite the existing profile + this.profile.mappings = this.getMappingsFromGrid() + this._columnMappingProfileService.update(this.orgId, this.profile).subscribe() + } + + getMappingsFromGrid(): ColumnMapping[] { const mappings: ColumnMapping[] = [] this.gridApi.forEachNode((node) => { const mapping = this.formatRowToMapping(node.data) if (mapping) mappings.push(mapping) }) - this.profile.mappings = mappings - this._columnMappingProfileService.update(this.orgId, this.profile).subscribe() + return mappings } formatRowToMapping(row: Record): ColumnMapping { @@ -265,7 +272,6 @@ export class MapDataComponent implements OnChanges, OnDestroy { } createProfile() { - console.log('create profile') const profileType: ColumnMappingProfileType = this.importFile.source_type === 'BuildingSync' ? 'BuildingSync Custom' : 'Normal' const profileTypes: ColumnMappingProfileType[] = profileType === 'BuildingSync Custom' ? ['BuildingSync Default', 'BuildingSync Custom'] : ['Normal'] const dialogRef = this._dialog.open(CreateProfileComponent, { @@ -274,6 +280,7 @@ export class MapDataComponent implements OnChanges, OnDestroy { existingNames: this.columnMappingProfiles.map((p) => p.name), orgId: this.orgId, profileType, + mappings: this.getMappingsFromGrid(), }, }) diff --git a/src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.ts b/src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.ts index 9aec2747..7060627c 100644 --- a/src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.ts +++ b/src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.ts @@ -6,7 +6,7 @@ import { MatDividerModule } from '@angular/material/divider' import { MatFormFieldModule } from '@angular/material/form-field' import { MatIconModule } from '@angular/material/icon' import { MatInputModule } from '@angular/material/input' -import type { ColumnMappingProfile, ColumnMappingProfileType } from '@seed/api/column_mapping_profile' +import type { ColumnMapping, ColumnMappingProfile, ColumnMappingProfileType } from '@seed/api/column_mapping_profile' import { ColumnMappingProfileService } from '@seed/api/column_mapping_profile' import { SEEDValidators } from '@seed/validators' @@ -31,7 +31,7 @@ export class CreateProfileComponent { profileName = '' profile: ColumnMappingProfile - data = inject(MAT_DIALOG_DATA) as { orgId: number; profileType: ColumnMappingProfileType; existingNames: string[] } + data = inject(MAT_DIALOG_DATA) as { orgId: number; profileType: ColumnMappingProfileType; mappings: ColumnMapping[]; existingNames: string[] } form = new FormGroup({ name: new FormControl('', [Validators.required, SEEDValidators.uniqueValue(this.data.existingNames)]), }) @@ -40,7 +40,7 @@ export class CreateProfileComponent { this.profile = { name: this.form.value.name, profile_type: this.data.profileType, - mappings: [], + mappings: this.data.mappings, } as ColumnMappingProfile this._columnMappingProfileService.create(this.data.orgId, this.profile) diff --git a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html index 39e866c9..838e36cc 100644 --- a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html +++ b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html @@ -5,22 +5,19 @@ - +
-
-
-
Access Level Info
+
+
- -
+
-
+ @if (propertyResults.length) { -
+
- Properties + Properties +
+
+
+
Access Level Info
+
+ } @if (taxlotResults.length) { -
+ +
- Tax Lots + Tax Lots +
+
+
+
Access Level Info
+
() + @Output() backToMapping = new EventEmitter() private _configService = inject(ConfigService) private _dataQualityService = inject(DataQualityService) diff --git a/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts b/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts index a357adea..4c66d495 100644 --- a/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts +++ b/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts @@ -10,6 +10,7 @@ import { ProgressBarComponent } from '@seed/components' import type { CheckProgressLoopParams} from '@seed/services/uploader' import { UploaderService } from '@seed/services/uploader' import { ResultsComponent } from './results.component' +import { InventoryType } from 'app/modules/inventory' @Component({ selector: 'seed-match-merge', @@ -25,7 +26,7 @@ import { ResultsComponent } from './results.component' export class MatchMergeComponent implements OnDestroy { @Input() importFileId: number @Input() orgId: number - @Input() inventoryType + @Input() inventoryType: InventoryType private _mappingService = inject(MappingService) private _uploaderService = inject(UploaderService) diff --git a/src/app/modules/datasets/data-mappings/step4/results.component.html b/src/app/modules/datasets/data-mappings/step4/results.component.html index 496c5133..d0dfc350 100644 --- a/src/app/modules/datasets/data-mappings/step4/results.component.html +++ b/src/app/modules/datasets/data-mappings/step4/results.component.html @@ -1,7 +1,12 @@
@if ( matchingResults) { -
+
+ + + @@ -54,7 +54,7 @@
- diff --git a/src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts b/src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts index 3a07adc8..658d92f3 100644 --- a/src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts +++ b/src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts @@ -16,10 +16,9 @@ import { MatSelectModule } from '@angular/material/select' import { MatStepper, MatStepperModule } from '@angular/material/stepper' import { Router, RouterModule } from '@angular/router' import { Cycle } from '@seed/api/cycle' -import { CycleService } from '@seed/api/cycle/cycle.service' import type { Dataset } from '@seed/api/dataset' -import { DatasetService } from '@seed/api/dataset' -import { ProgressResponse } from '@seed/api/progress' +import { OrganizationService, OrganizationUserSettings } from '@seed/api/organization' +import { UserService } from '@seed/api/user' import { ProgressBarComponent } from '@seed/components' import { ErrorService } from '@seed/services' import { ProgressBarObj, UploaderService } from '@seed/services/uploader' @@ -53,9 +52,10 @@ export class PropertyTaxlotUploadComponent implements AfterViewInit, OnDestroy { @Input() dataset: Dataset @Input() orgId: number @Output() dismissModal = new EventEmitter() - private _datasetService = inject(DatasetService) - private _cycleService = inject(CycleService) + + private _organizationService = inject(OrganizationService) private _uploaderService = inject(UploaderService) + private _userService = inject(UserService) private _errorService = inject(ErrorService) private _router = inject(Router) private _snackBar = inject(SnackBarService) @@ -65,7 +65,7 @@ export class PropertyTaxlotUploadComponent implements AfterViewInit, OnDestroy { file: File fileId: number inProgress = false - uploading = false + orgUserId: number progressBarObj: ProgressBarObj = { message: [], progress: 0, @@ -76,14 +76,25 @@ export class PropertyTaxlotUploadComponent implements AfterViewInit, OnDestroy { progressLastChecked: null, } sourceType: 'Assessed Raw' | 'GeoJSON' | 'BuildingSync Raw' + uploading = false + userSettings: OrganizationUserSettings = {} form = new FormGroup({ cycleId: new FormControl(null, Validators.required), multiCycle: new FormControl(false), }) - + ngAfterViewInit() { this.form.patchValue({ cycleId: this.cycles[0]?.id }) + this._userService.currentUser$ + .pipe( + takeUntil(this._unsubscribeAll$), + tap((user) => { + this.orgUserId = user.org_user_id + this.userSettings = user.settings + }), + ) + .subscribe() } step1(fileList: FileList) { @@ -91,6 +102,8 @@ export class PropertyTaxlotUploadComponent implements AfterViewInit, OnDestroy { const cycleId = this.form.get('cycleId')?.value const multiCycle = this.form.get('multiCycle')?.value this.uploading = true + this.userSettings.cycleId = cycleId + this._organizationService.updateOrganizationUser(this.orgUserId, this.orgId, this.userSettings).subscribe() return this._uploaderService .fileUpload(this.orgId, this.file, this.sourceType, this.dataset.id.toString()) diff --git a/src/app/modules/datasets/dataset/dataset.component.ts b/src/app/modules/datasets/dataset/dataset.component.ts index 1b283ef7..902f6904 100644 --- a/src/app/modules/datasets/dataset/dataset.component.ts +++ b/src/app/modules/datasets/dataset/dataset.component.ts @@ -58,7 +58,6 @@ export class DatasetComponent implements OnDestroy, OnInit { tap((cycles) => { this.cycles = cycles this.cyclesMap = cycles.reduce((acc, c) => ({ ...acc, [c.id]: c.name }), {}) - console.log('cyclesMap', this.cyclesMap) }), ) } @@ -130,7 +129,6 @@ export class DatasetComponent implements OnDestroy, OnInit { this.downloadDocument(importFile.file, importFile.uploaded_filename) } else if (action === 'dataMapping') { void this._router.navigate(['/data/mappings/', importFile.id]) - console.log('data mapping', importFile) } else if (action === 'dataPairing') { console.log('data pairing', importFile) } @@ -150,7 +148,6 @@ export class DatasetComponent implements OnDestroy, OnInit { } downloadDocument(file: string, filename: string) { - console.log('file', file) const a = document.createElement('a') const url = file a.href = url diff --git a/src/app/modules/datasets/datasets.component.ts b/src/app/modules/datasets/datasets.component.ts index 4c10365d..451136cf 100644 --- a/src/app/modules/datasets/datasets.component.ts +++ b/src/app/modules/datasets/datasets.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common' -import type { OnInit } from '@angular/core' +import type { OnDestroy, OnInit } from '@angular/core' import { Component, inject, ViewEncapsulation } from '@angular/core' import { MatButtonModule } from '@angular/material/button' import { MatDialog } from '@angular/material/dialog' @@ -7,7 +7,7 @@ import { MatIconModule } from '@angular/material/icon' import { ActivatedRoute, Router } from '@angular/router' import { AgGridAngular } from 'ag-grid-angular' import type { CellClickedEvent, ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' -import { filter, switchMap, tap } from 'rxjs' +import { combineLatest, filter, Subject, switchMap, takeUntil, tap } from 'rxjs' import type { Cycle } from '@seed/api/cycle' import { CycleService } from '@seed/api/cycle/cycle.service' import { type Dataset, DatasetService } from '@seed/api/dataset' @@ -22,7 +22,6 @@ import { FormModalComponent } from './modal/form-modal.component' selector: 'seed-data', templateUrl: './datasets.component.html', encapsulation: ViewEncapsulation.None, - // changeDetection: ChangeDetectionStrategy.OnPush, imports: [ AgGridAngular, CommonModule, @@ -31,7 +30,7 @@ import { FormModalComponent } from './modal/form-modal.component' PageComponent, ], }) -export class DatasetsComponent implements OnInit { +export class DatasetsComponent implements OnDestroy, OnInit { private _configService = inject(ConfigService) private _cycleService = inject(CycleService) private _datasetService = inject(DatasetService) @@ -39,6 +38,7 @@ export class DatasetsComponent implements OnInit { private _router = inject(Router) private _userService = inject(UserService) private _dialog = inject(MatDialog) + private readonly _unsubscribeAll$ = new Subject() columnDefs: ColDef[] cycles: Cycle[] = [] datasets: Dataset[] @@ -61,14 +61,17 @@ export class DatasetsComponent implements OnInit { this.orgId = orgId this._datasetService.list(orgId) }), - switchMap(() => this._cycleService.cycles$), - tap((cycles) => { this.cycles = cycles }), - switchMap(() => this._datasetService.datasets$), - tap((datasets) => { + switchMap(() => combineLatest([ + this._cycleService.cycles$, + this._datasetService.datasets$, + ])), + tap(([cycles, datasets]) => { + this.cycles = cycles this.datasets = datasets.sort((a, b) => naturalSort(a.name, b.name)) this.existingNames = datasets.map((ds) => ds.name) this.setColumnDefs() }), + takeUntil(this._unsubscribeAll$), ).subscribe() } @@ -163,4 +166,9 @@ export class DatasetsComponent implements OnInit { trackByFn(_index: number, { id }: Dataset) { return id } + + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } } diff --git a/src/app/modules/inventory-list/list/grid/grid.component.ts b/src/app/modules/inventory-list/list/grid/grid.component.ts index 6495f2f4..efe1129b 100644 --- a/src/app/modules/inventory-list/list/grid/grid.component.ts +++ b/src/app/modules/inventory-list/list/grid/grid.component.ts @@ -89,12 +89,13 @@ export class InventoryGridComponent implements OnChanges { const target = event.event.target as HTMLElement const action = target.getAttribute('data-action') as 'detail' | 'notes' | 'meters' | null if (!action) return - const { property_view_id } = event.data as { property_view_id: string; file: string; filename: string } + const { property_view_id, taxlot_view_id } = event.data as { property_view_id: string; taxlot_view_id: string } + const viewId = property_view_id || taxlot_view_id const urlMap = { - detail: [`/${this.inventoryType}`, property_view_id], - notes: [`/${this.inventoryType}`, property_view_id, 'notes'], - meters: [`/${this.inventoryType}`, property_view_id, 'meters'], + detail: [`/${this.inventoryType}`, viewId], + notes: [`/${this.inventoryType}`, viewId, 'notes'], + meters: [`/${this.inventoryType}`, viewId, 'meters'], } return void this._router.navigate(urlMap[action]) @@ -148,7 +149,7 @@ export class InventoryGridComponent implements OnChanges { actionRenderer = (value, icon: string, action: string) => { if (!value) return '' - // Allow a single letter to be passed as an indicator + // Allow a single letter to be passed as an indicator (like G for groups) if (icon.length === 1) { return `${icon}` } From 2f5d86c6aea6e1191114ce8276e02992e0b8e38b Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 9 Jul 2025 19:22:35 +0000 Subject: [PATCH 36/37] prettier --- .../ag-grid/autocomplete.component.html | 9 +- .../progress/progress-bar.component.html | 16 ++-- .../data-quality/results-modal.component.html | 6 +- .../data-mappings/data-mapping.component.html | 62 +++++------- .../data-mappings/help.component.html | 50 ++++------ .../step1/map-data.component.html | 96 +++++++++---------- .../step1/modal/create-profile.component.html | 4 +- .../step3/save-mappings.component.html | 64 ++++++------- .../step4/match-merge.component.html | 18 ++-- .../step4/results.component.html | 25 ++--- .../data-upload-modal.component.html | 8 +- .../property-taxlot-upload.component.html | 59 +++++------- .../datasets/dataset/dataset.component.html | 4 +- .../modules/datasets/datasets.component.html | 28 +++--- .../datasets/modal/form-modal.component.html | 4 +- .../green-button-upload-modal.component.html | 6 +- .../modal/more-actions-modal.component.html | 14 +-- 17 files changed, 207 insertions(+), 266 deletions(-) diff --git a/src/@seed/components/ag-grid/autocomplete.component.html b/src/@seed/components/ag-grid/autocomplete.component.html index 61e84dd6..f24ce47b 100644 --- a/src/@seed/components/ag-grid/autocomplete.component.html +++ b/src/@seed/components/ag-grid/autocomplete.component.html @@ -1,10 +1,5 @@ - + @for (option of filteredOptions; track $index) { @@ -12,4 +7,4 @@ } - \ No newline at end of file + diff --git a/src/@seed/components/progress/progress-bar.component.html b/src/@seed/components/progress/progress-bar.component.html index 8ffeecdd..1e31e892 100644 --- a/src/@seed/components/progress/progress-bar.component.html +++ b/src/@seed/components/progress/progress-bar.component.html @@ -11,8 +11,12 @@
}
- - + +
@if (showSubProgress && subProgress && subProgress < 100) { @@ -22,13 +26,13 @@
{{ subTitle }}
@if (subProgress) { -
- {{ subProgressString }} -
+
+ {{ subProgressString }} +
}
- +
}
diff --git a/src/app/modules/data-quality/results-modal.component.html b/src/app/modules/data-quality/results-modal.component.html index 9bcc354f..777c8ac1 100644 --- a/src/app/modules/data-quality/results-modal.component.html +++ b/src/app/modules/data-quality/results-modal.component.html @@ -12,13 +12,13 @@ [domLayout]="'autoHeight'" [pagination]="true" [paginationPageSize]="10" - > + > } @else {
No warnings or errors
}
-
+
-
\ No newline at end of file +
diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.html b/src/app/modules/datasets/data-mappings/data-mapping.component.html index 82b2a9d6..4f57ef1a 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.html +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.html @@ -8,18 +8,15 @@ }" >
- -
- @@ -27,7 +24,6 @@ - @@ -36,51 +32,39 @@ - + - + - + - - - - - \ No newline at end of file + diff --git a/src/app/modules/datasets/data-mappings/help.component.html b/src/app/modules/datasets/data-mappings/help.component.html index 4727e294..dc2d5ae9 100644 --- a/src/app/modules/datasets/data-mappings/help.component.html +++ b/src/app/modules/datasets/data-mappings/help.component.html @@ -1,55 +1,37 @@
-
- MAPPING YOUR DATA TO SEED -
- -
- It is necessary to map your field names to SEED field names. You can select from the list that appears as you start to - type, which is based on the Building Energy Data Exchange Specification (BEDES), or you can type in your own name, as - well as typing in the field name from the original datafile. -
+
MAPPING YOUR DATA TO SEED
- In addition, you need to specify where the field should be associated with Tax Lot data or Property data. This will - affect how the data is matched and merged, as well as how it is displayed in the Inventory view. + It is necessary to map your field names to SEED field names. You can select from the list that appears as you start to type, which is + based on the Building Energy Data Exchange Specification (BEDES), or you can type in your own name, as well as typing in the field name + from the original datafile.
- Column Mapping Profiles can be used to help you easily and consistently map your data. Note that file header columns - defined in the profile must match exactly (spaces, lowercase, uppercase, etc.) in order for the corresponding SEED - column information to be used. + In addition, you need to specify where the field should be associated with Tax Lot data or Property data. This will affect how the data + is matched and merged, as well as how it is displayed in the Inventory view.
- Field names for matching Properties: Custom ID 1, PM Property ID - + Column Mapping Profiles can be used to help you easily and consistently map your data. Note that file header columns defined in the + profile must match exactly (spaces, lowercase, uppercase, etc.) in order for the corresponding SEED column information to be used.
-
- Field names for matching Tax Lots: Custom ID 1, Jurisdiction Tax Lot ID -
+
Field names for matching Properties: Custom ID 1, PM Property ID
+ +
Field names for matching Tax Lots: Custom ID 1, Jurisdiction Tax Lot ID
- If there are fields in the datafile mapped to these names, the program will attempt to match on the corresponding values - in existing records. All of these fields must have the same values between records for the records to match. + If there are fields in the datafile mapped to these names, the program will attempt to match on the corresponding values in existing + records. All of these fields must have the same values between records for the records to match.
- Matches within the same cycle will be merged together, while matches in different cycles will be associated for - cross-cycle analysis. + Matches within the same cycle will be merged together, while matches in different cycles will be associated for cross-cycle analysis.
- When you click the Map Your Data button, the program will show a grid with the new field names as the column headings - and your data in the rows. In that view, you can still come back to the initial mapping screen and change the field - mapping. + When you click the Map Your Data button, the program will show a grid with the new field names as the column headings and your data in + the rows. In that view, you can still come back to the initial mapping screen and change the field mapping.
-
- - - - - - - diff --git a/src/app/modules/datasets/data-mappings/step1/map-data.component.html b/src/app/modules/datasets/data-mappings/step1/map-data.component.html index 749a006e..51d6bd11 100644 --- a/src/app/modules/datasets/data-mappings/step1/map-data.component.html +++ b/src/app/modules/datasets/data-mappings/step1/map-data.component.html @@ -1,64 +1,64 @@
- -
-
Cycle:
-
{{ cycle?.name }}
-
-
- - -
+
+
Cycle:
+
{{ cycle?.name }}
+
+
+ +
+
Column Profile:
+ + + @for (profile of columnMappingProfiles; track profile.id) { + {{ profile.name }} + } + + + + + + + + + + + +
-
- - - Properties - Tax Lots - - - -
+
+ + + Properties + Tax Lots + + +
-
-
-
-
Editable Cell
+
+
+
+
Editable Cell
- @if (errorMessages.length ) { - + @if (errorMessages.length) { +
    @for (error of errorMessages; track $index) {
  • {{ error }}
  • @@ -68,7 +68,7 @@ }
- +
- \ No newline at end of file + diff --git a/src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.html b/src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.html index 3e8e3600..a4549cf8 100644 --- a/src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.html +++ b/src/app/modules/datasets/data-mappings/step1/modal/create-profile.component.html @@ -7,7 +7,6 @@
- Name @@ -15,7 +14,6 @@ This name already exists. } -
@@ -27,4 +25,4 @@ -
\ No newline at end of file +
diff --git a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html index 838e36cc..06f8c2a7 100644 --- a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html +++ b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html @@ -1,41 +1,43 @@ -
+
Cycle:
{{ cycle?.name }}
- - - -
+ +
- +
-
- - + + (click)="saveData()" + color="primary" + mat-raised-button + > + Save Data +
@if (propertyResults.length) { -
+
Properties
-
-
-
Access Level Info
+
+
+
Access Level Info
@@ -45,20 +47,19 @@ [theme]="gridTheme$ | async" [pagination]="true" [paginationPageSize]="20" - [domLayout]="'autoHeight'" + [domLayout]="'autoHeight'" > - - -} + + +} @if (taxlotResults.length) { - -
+
Tax Lots
-
-
-
Access Level Info
+
+
+
Access Level Info
} @@ -80,5 +81,4 @@
- -} \ No newline at end of file +} diff --git a/src/app/modules/datasets/data-mappings/step4/match-merge.component.html b/src/app/modules/datasets/data-mappings/step4/match-merge.component.html index cb23f6f5..a4c07be5 100644 --- a/src/app/modules/datasets/data-mappings/step4/match-merge.component.html +++ b/src/app/modules/datasets/data-mappings/step4/match-merge.component.html @@ -1,14 +1,14 @@
@if (inProgress) { + [progress]="progressBarObj.progress" + [total]="progressBarObj.total" + [title]="progressBarObj.statusMessage" + [showSubProgress]="true" + [subProgress]="subProgressBarObj.progress" + [subTitle]="subProgressBarObj.statusMessage" + [subTotal]="subProgressBarObj.total" + > } @else { } -
\ No newline at end of file +
diff --git a/src/app/modules/datasets/data-mappings/step4/results.component.html b/src/app/modules/datasets/data-mappings/step4/results.component.html index d0dfc350..bf78791b 100644 --- a/src/app/modules/datasets/data-mappings/step4/results.component.html +++ b/src/app/modules/datasets/data-mappings/step4/results.component.html @@ -1,16 +1,11 @@ -
- - @if ( matchingResults) { +
-
+
Properties
- +
-
+
Tax Lots
- +
} - - -
\ No newline at end of file +
diff --git a/src/app/modules/datasets/data-upload/data-upload-modal.component.html b/src/app/modules/datasets/data-upload/data-upload-modal.component.html index 2bab5be2..2fbf5148 100644 --- a/src/app/modules/datasets/data-upload/data-upload-modal.component.html +++ b/src/app/modules/datasets/data-upload/data-upload-modal.component.html @@ -1,7 +1,8 @@
+ class="flex h-10 w-10 flex-0 items-center justify-center rounded-full bg-primary-100 text-primary-600 sm:mr-4 dark:bg-primary-600 dark:text-primary-50" + >
@@ -11,7 +12,8 @@
+ (click)="dismiss()" + >
@@ -22,4 +24,4 @@ [dataset]="data.dataset" [orgId]="data.orgId" (dismissModal)="dismiss()" -> \ No newline at end of file +> diff --git a/src/app/modules/datasets/data-upload/property-taxlot-upload.component.html b/src/app/modules/datasets/data-upload/property-taxlot-upload.component.html index 108e56ce..a8d1677b 100644 --- a/src/app/modules/datasets/data-upload/property-taxlot-upload.component.html +++ b/src/app/modules/datasets/data-upload/property-taxlot-upload.component.html @@ -1,71 +1,64 @@ -
-
- +
- {{ form.get('multiCycle').value ? 'Default Cycle' : 'Cycle'}} + {{ form.get('multiCycle').value ? 'Default Cycle' : 'Cycle' }} @for (cycle of cycles; track $index) { - {{ cycle.name }} + {{ cycle.name }} } - - Multi-Cycle - + Multi-Cycle
- - -
+ +
- .csv, .xls, .xslx
-
Note: only the first sheet of multi-sheet Excel files will be imported.
+
Note: only the first sheet of multi-sheet Excel files will be imported.
-
- .geojson, .json
- +
- .xml
- -
- + @if (uploading) { } @else { @@ -73,28 +66,22 @@
} - + - - @if (inProgress) { - - } + + @if (inProgress) { + + } - -
+
-
- - \ No newline at end of file + diff --git a/src/app/modules/datasets/dataset/dataset.component.html b/src/app/modules/datasets/dataset/dataset.component.html index 84a6b9f6..70b1fb8e 100644 --- a/src/app/modules/datasets/dataset/dataset.component.html +++ b/src/app/modules/datasets/dataset/dataset.component.html @@ -3,7 +3,7 @@ title: 'Dataset', subTitle: datasetName$ | async, titleIcon: 'fa-solid:sitemap', - breadcrumbs: ['Dataset', 'Detail'] + breadcrumbs: ['Dataset', 'Detail'], }" >
@@ -20,4 +20,4 @@ > }
- \ No newline at end of file + diff --git a/src/app/modules/datasets/datasets.component.html b/src/app/modules/datasets/datasets.component.html index 4901d1d5..3eda6111 100644 --- a/src/app/modules/datasets/datasets.component.html +++ b/src/app/modules/datasets/datasets.component.html @@ -7,18 +7,18 @@ actionText: 'Create Dataset', }" > -
- @if (datasets.length) { - - } -
+
+ @if (datasets.length) { + + } +
diff --git a/src/app/modules/datasets/modal/form-modal.component.html b/src/app/modules/datasets/modal/form-modal.component.html index 63a93e85..36b4eaa5 100644 --- a/src/app/modules/datasets/modal/form-modal.component.html +++ b/src/app/modules/datasets/modal/form-modal.component.html @@ -9,7 +9,7 @@ Dataset Name @if (form.controls.name?.hasError('valueExists')) { - This name already exists. + This name already exists. } @@ -21,4 +21,4 @@ -
\ No newline at end of file +
diff --git a/src/app/modules/inventory-detail/meters/modal/green-button-upload-modal.component.html b/src/app/modules/inventory-detail/meters/modal/green-button-upload-modal.component.html index c4c8858a..f798b529 100644 --- a/src/app/modules/inventory-detail/meters/modal/green-button-upload-modal.component.html +++ b/src/app/modules/inventory-detail/meters/modal/green-button-upload-modal.component.html @@ -86,11 +86,7 @@ @if (inProgress) { - + } diff --git a/src/app/modules/inventory-list/list/modal/more-actions-modal.component.html b/src/app/modules/inventory-list/list/modal/more-actions-modal.component.html index c15e3974..0281e76c 100644 --- a/src/app/modules/inventory-list/list/modal/more-actions-modal.component.html +++ b/src/app/modules/inventory-list/list/modal/more-actions-modal.component.html @@ -39,16 +39,16 @@ @if (showProgress) { -
+
- + class="w-full" + [progress]="progressBarObj.progress" + [total]="progressBarObj.total" + [title]="progressBarObj.statusMessage || 'In Progress...'" + > +
} -
From 7b0d32c263878b5b26b7e129ae87c03b95b288f7 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Wed, 9 Jul 2025 19:35:46 +0000 Subject: [PATCH 37/37] lint --- .../api/data-quality/data-quality.service.ts | 2 +- src/@seed/api/dataset/dataset.service.ts | 4 ++-- src/@seed/api/mapping/mapping.service.ts | 10 ++++---- src/@seed/components/ag-grid/index.ts | 2 +- .../services/uploader/uploader.service.ts | 6 ++--- .../data-quality/results-modal.component.ts | 6 ++--- .../data-mappings/data-mapping.component.ts | 3 +-- .../data-mappings/step1/column-defs.ts | 6 ++--- .../data-mappings/step1/map-data.component.ts | 4 ++-- .../step3/save-mappings.component.ts | 2 +- .../step4/match-merge.component.ts | 4 ++-- .../data-upload-modal.component.ts | 2 +- .../property-taxlot-upload.component.ts | 23 +++++++++++-------- src/app/modules/datasets/index.ts | 2 +- 14 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/@seed/api/data-quality/data-quality.service.ts b/src/@seed/api/data-quality/data-quality.service.ts index 262dce0e..f26f5ac4 100644 --- a/src/@seed/api/data-quality/data-quality.service.ts +++ b/src/@seed/api/data-quality/data-quality.service.ts @@ -4,8 +4,8 @@ import { catchError, map, type Observable, ReplaySubject, switchMap, tap } from import { OrganizationService } from '@seed/api/organization' import { ErrorService } from '@seed/services' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' -import type { DataQualityResults, DataQualityResultsResponse, Rule } from './data-quality.types' import type { DQCProgressResponse } from '../progress' +import type { DataQualityResults, DataQualityResultsResponse, Rule } from './data-quality.types' @Injectable({ providedIn: 'root' }) export class DataQualityService { diff --git a/src/@seed/api/dataset/dataset.service.ts b/src/@seed/api/dataset/dataset.service.ts index c032b0ce..68b8538f 100644 --- a/src/@seed/api/dataset/dataset.service.ts +++ b/src/@seed/api/dataset/dataset.service.ts @@ -3,10 +3,10 @@ import { HttpClient } from '@angular/common/http' import { inject, Injectable } from '@angular/core' import type { Observable } from 'rxjs' import { catchError, map, ReplaySubject, tap } from 'rxjs' -import { UserService } from '../user' -import type { CountDatasetsResponse, Dataset, DatasetResponse, ImportFile, ImportFileResponse, ListDatasetsResponse } from './dataset.types' import { ErrorService } from '@seed/services' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' +import { UserService } from '../user' +import type { CountDatasetsResponse, Dataset, DatasetResponse, ImportFile, ImportFileResponse, ListDatasetsResponse } from './dataset.types' @Injectable({ providedIn: 'root' }) export class DatasetService { diff --git a/src/@seed/api/mapping/mapping.service.ts b/src/@seed/api/mapping/mapping.service.ts index 01862c9c..60cc1040 100644 --- a/src/@seed/api/mapping/mapping.service.ts +++ b/src/@seed/api/mapping/mapping.service.ts @@ -1,12 +1,12 @@ -import type { HttpErrorResponse } from '@angular/common/http'; +import type { HttpErrorResponse } from '@angular/common/http' import { HttpClient } from '@angular/common/http' import { inject, Injectable } from '@angular/core' +import { catchError, map, type Observable } from 'rxjs' import { ErrorService } from '@seed/services' +import type { MappedData, MappingResultsResponse } from '../dataset' +import type { ProgressResponse, SubProgressResponse } from '../progress' import { UserService } from '../user' -import { catchError, map, tap, type Observable } from 'rxjs' import type { FirstFiveRowsResponse, MappingSuggestionsResponse, MatchingResultsResponse, RawColumnNamesResponse } from './mapping.types' -import { MappedData, MappingResultsResponse } from '../dataset' -import type { ProgressResponse, SubProgressResponse } from '../progress' @Injectable({ providedIn: 'root' }) export class MappingService { @@ -28,7 +28,7 @@ export class MappingService { const url = `/api/v3/import_files/${importFileId}/raw_column_names/?organization_id=${orgId}` return this._httpClient.get(url) .pipe( - map(({ raw_columns }) => raw_columns ), + map(({ raw_columns }) => raw_columns), catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error fetching raw column names') }), diff --git a/src/@seed/components/ag-grid/index.ts b/src/@seed/components/ag-grid/index.ts index 82292220..84938b4e 100644 --- a/src/@seed/components/ag-grid/index.ts +++ b/src/@seed/components/ag-grid/index.ts @@ -1 +1 @@ -export * from './editHeader.component' \ No newline at end of file +export * from './editHeader.component' diff --git a/src/@seed/services/uploader/uploader.service.ts b/src/@seed/services/uploader/uploader.service.ts index b1cb1aae..2c106d94 100644 --- a/src/@seed/services/uploader/uploader.service.ts +++ b/src/@seed/services/uploader/uploader.service.ts @@ -2,7 +2,7 @@ import type { HttpErrorResponse } from '@angular/common/http' import { HttpClient } from '@angular/common/http' import { inject, Injectable } from '@angular/core' import type { Observable } from 'rxjs' -import { catchError, combineLatest, filter, finalize, interval, map, of, repeat, startWith, Subject, switchMap, takeUntil, takeWhile, tap, throwError } from 'rxjs' +import { catchError, combineLatest, finalize, interval, of, Subject, switchMap, takeUntil, takeWhile, tap, throwError } from 'rxjs' import type { ProgressResponse } from '@seed/api/progress' import { ErrorService } from '../error' import type { @@ -70,7 +70,7 @@ export class UploaderService { } /* - * Check the progress of Main Progress and its Sub Progress + * Check the progress of Main Progress and its Sub Progress * Main progress will run until it completes * Sub Progresses can complete several times and will run continuously until Main Progress is completed * the stop$ stream is used to end the Sub Progress stream @@ -106,8 +106,6 @@ export class UploaderService { ) } - - greenButtonMetersPreview(orgId: number, viewId: number, systemId: number, fileId: number): Observable { const url = `/api/v3/import_files/${fileId}/greenbutton_meters_preview/` const params: Record = { organization_id: orgId } diff --git a/src/app/modules/data-quality/results-modal.component.ts b/src/app/modules/data-quality/results-modal.component.ts index 64fd81be..d2387903 100644 --- a/src/app/modules/data-quality/results-modal.component.ts +++ b/src/app/modules/data-quality/results-modal.component.ts @@ -5,12 +5,12 @@ import { MatButtonModule } from '@angular/material/button' import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' import { MatDividerModule } from '@angular/material/divider' import { MatIconModule } from '@angular/material/icon' -import type { DataQualityResults } from '@seed/api/data-quality' -import { DataQualityService } from '@seed/api/data-quality' -import { ConfigService } from '@seed/services' import { AgGridAngular } from 'ag-grid-angular' import type { ColDef } from 'ag-grid-community' import { Subject, takeUntil, tap } from 'rxjs' +import type { DataQualityResults } from '@seed/api/data-quality' +import { DataQualityService } from '@seed/api/data-quality' +import { ConfigService } from '@seed/services' @Component({ selector: 'seed-data-quality-results', diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.ts b/src/app/modules/datasets/data-mappings/data-mapping.component.ts index c8618c1c..e9f62ae4 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.ts +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ import { CommonModule } from '@angular/common' import type { OnDestroy, OnInit } from '@angular/core' import { Component, inject, ViewChild } from '@angular/core' @@ -23,7 +22,7 @@ import type { ImportFile, MappingResultsResponse } from '@seed/api/dataset' import { DatasetService } from '@seed/api/dataset' import type { MappingSuggestionsResponse } from '@seed/api/mapping' import { MappingService } from '@seed/api/mapping' -import type { Organization } from '@seed/api/organization'; +import type { Organization } from '@seed/api/organization' import { OrganizationService } from '@seed/api/organization' import type { ProgressResponse } from '@seed/api/progress' import { UserService } from '@seed/api/user' diff --git a/src/app/modules/datasets/data-mappings/step1/column-defs.ts b/src/app/modules/datasets/data-mappings/step1/column-defs.ts index b39218ec..d474d645 100644 --- a/src/app/modules/datasets/data-mappings/step1/column-defs.ts +++ b/src/app/modules/datasets/data-mappings/step1/column-defs.ts @@ -89,9 +89,9 @@ export const buildColumnDefs = ( field: 'to_field_display_name', headerName: 'SEED Header', cellEditor: AutocompleteCellComponent, - cellEditorParams: (params) => { - return { values: getColumnOptions(params, propertyColumnNames, taxlotColumnNames)} - } , + cellEditorParams: (params: ICellRendererParams) => { + return { values: getColumnOptions(params, propertyColumnNames, taxlotColumnNames) } + }, headerComponent: EditHeaderComponent, headerComponentParams: { name: 'SEED Header', diff --git a/src/app/modules/datasets/data-mappings/step1/map-data.component.ts b/src/app/modules/datasets/data-mappings/step1/map-data.component.ts index 2b6aa226..5398b9fc 100644 --- a/src/app/modules/datasets/data-mappings/step1/map-data.component.ts +++ b/src/app/modules/datasets/data-mappings/step1/map-data.component.ts @@ -18,8 +18,8 @@ import { AgGridAngular } from 'ag-grid-angular' import type { CellValueChangedEvent, ColDef, GridApi, GridReadyEvent, RowNode } from 'ag-grid-community' import { Subject, switchMap, take } from 'rxjs' import { type Column } from '@seed/api/column' -import type { ColumnMappingProfileType } from '@seed/api/column_mapping_profile' -import { type ColumnMapping, type ColumnMappingProfile, ColumnMappingProfileService } from '@seed/api/column_mapping_profile' +import type { ColumnMapping, ColumnMappingProfile, ColumnMappingProfileType } from '@seed/api/column_mapping_profile' +import { ColumnMappingProfileService } from '@seed/api/column_mapping_profile' import type { Cycle } from '@seed/api/cycle' import type { DataMappingRow, ImportFile } from '@seed/api/dataset' import type { MappingSuggestionsResponse } from '@seed/api/mapping' diff --git a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts index 221e0a25..0df05ba1 100644 --- a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts +++ b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.ts @@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common' import type { OnChanges, OnDestroy, SimpleChanges } from '@angular/core' import { Component, EventEmitter, inject, Input, Output } from '@angular/core' import { MatButtonModule } from '@angular/material/button' -import { MatDialog } from '@angular/material/dialog'; +import { MatDialog } from '@angular/material/dialog' import { MatDividerModule } from '@angular/material/divider' import { MatIconModule } from '@angular/material/icon' import { MatProgressBarModule } from '@angular/material/progress-bar' diff --git a/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts b/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts index 4c66d495..b7a3c6cd 100644 --- a/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts +++ b/src/app/modules/datasets/data-mappings/step4/match-merge.component.ts @@ -7,10 +7,10 @@ import { of, Subject, switchMap, takeUntil } from 'rxjs' import { MappingService } from '@seed/api/mapping' import type { ProgressResponse, SubProgressResponse } from '@seed/api/progress' import { ProgressBarComponent } from '@seed/components' -import type { CheckProgressLoopParams} from '@seed/services/uploader' +import type { CheckProgressLoopParams } from '@seed/services/uploader' import { UploaderService } from '@seed/services/uploader' +import type { InventoryType } from 'app/modules/inventory' import { ResultsComponent } from './results.component' -import { InventoryType } from 'app/modules/inventory' @Component({ selector: 'seed-match-merge', diff --git a/src/app/modules/datasets/data-upload/data-upload-modal.component.ts b/src/app/modules/datasets/data-upload/data-upload-modal.component.ts index 24b023df..59ab7586 100644 --- a/src/app/modules/datasets/data-upload/data-upload-modal.component.ts +++ b/src/app/modules/datasets/data-upload/data-upload-modal.component.ts @@ -3,9 +3,9 @@ import { Component, inject } from '@angular/core' import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog' import { MatDividerModule } from '@angular/material/divider' import { MatIconModule } from '@angular/material/icon' +import type { Cycle } from '@seed/api/cycle' import type { Dataset } from '@seed/api/dataset' import { PropertyTaxlotUploadComponent } from './property-taxlot-upload.component' -import { Cycle } from '@seed/api/cycle' @Component({ selector: 'seed-data-upload-modal', diff --git a/src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts b/src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts index 658d92f3..ce060824 100644 --- a/src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts +++ b/src/app/modules/datasets/data-upload/property-taxlot-upload.component.ts @@ -1,29 +1,32 @@ -import { StepperSelectionEvent } from '@angular/cdk/stepper' +import type { StepperSelectionEvent } from '@angular/cdk/stepper' import { CommonModule } from '@angular/common' -import { HttpErrorResponse } from '@angular/common/http' -import type { AfterViewInit, ElementRef, OnDestroy, OnInit } from '@angular/core' +import type { HttpErrorResponse } from '@angular/common/http' +import type { AfterViewInit, ElementRef, OnDestroy } from '@angular/core' import { Component, EventEmitter, inject, Input, Output, ViewChild } from '@angular/core' import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms' import { MatButtonModule } from '@angular/material/button' -import { MatCheckboxChange, MatCheckboxModule } from '@angular/material/checkbox' -import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog' +import { MatCheckboxModule } from '@angular/material/checkbox' +import { MatDialogModule } from '@angular/material/dialog' import { MatDividerModule } from '@angular/material/divider' import { MatFormFieldModule } from '@angular/material/form-field' import { MatIconModule } from '@angular/material/icon' import { MatInputModule } from '@angular/material/input' import { MatProgressBarModule } from '@angular/material/progress-bar' import { MatSelectModule } from '@angular/material/select' -import { MatStepper, MatStepperModule } from '@angular/material/stepper' +import type { MatStepper } from '@angular/material/stepper' +import { MatStepperModule } from '@angular/material/stepper' import { Router, RouterModule } from '@angular/router' -import { Cycle } from '@seed/api/cycle' +import { catchError, Subject, switchMap, takeUntil, tap } from 'rxjs' +import type { Cycle } from '@seed/api/cycle' import type { Dataset } from '@seed/api/dataset' -import { OrganizationService, OrganizationUserSettings } from '@seed/api/organization' +import type { OrganizationUserSettings } from '@seed/api/organization' +import { OrganizationService } from '@seed/api/organization' import { UserService } from '@seed/api/user' import { ProgressBarComponent } from '@seed/components' import { ErrorService } from '@seed/services' -import { ProgressBarObj, UploaderService } from '@seed/services/uploader' +import type { ProgressBarObj } from '@seed/services/uploader' +import { UploaderService } from '@seed/services/uploader' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' -import { catchError, Subject, switchMap, takeUntil, tap } from 'rxjs' @Component({ selector: 'seed-property-taxlot-upload', diff --git a/src/app/modules/datasets/index.ts b/src/app/modules/datasets/index.ts index 7a4e3855..944275fc 100644 --- a/src/app/modules/datasets/index.ts +++ b/src/app/modules/datasets/index.ts @@ -1,3 +1,3 @@ export * from './datasets.component' export * from './dataset' -export * from './data-mappings' \ No newline at end of file +export * from './data-mappings'