From ec172a25147a0c0b8c9e7dff9b1f4e33c6276ea3 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Mon, 21 Jul 2025 20:35:20 +0000 Subject: [PATCH 01/16] action group modal --- src/@seed/api/groups/groups.service.ts | 16 ++ .../api/organization/organization.service.ts | 12 ++ .../api/organization/organization.types.ts | 5 + src/@seed/components/index.ts | 1 + src/@seed/components/menu/index.ts | 1 + .../components/menu/menu-item.component.html | 14 ++ .../components/menu/menu-item.component.ts | 20 ++ .../list/actions/groups-modal.component.html | 59 +++++ .../list/actions/groups-modal.component.ts | 153 +++++++++++++ .../inventory-list/list/actions/index.ts | 1 + .../list/grid/actions.component.html | 204 ++++++++++++++++-- .../list/grid/actions.component.ts | 97 ++++----- .../list/grid/cell-header-menu.component.html | 2 +- src/styles/styles.scss | 10 + 14 files changed, 522 insertions(+), 73 deletions(-) create mode 100644 src/@seed/components/menu/index.ts create mode 100644 src/@seed/components/menu/menu-item.component.html create mode 100644 src/@seed/components/menu/menu-item.component.ts create mode 100644 src/app/modules/inventory-list/list/actions/groups-modal.component.html create mode 100644 src/app/modules/inventory-list/list/actions/groups-modal.component.ts create mode 100644 src/app/modules/inventory-list/list/actions/index.ts diff --git a/src/@seed/api/groups/groups.service.ts b/src/@seed/api/groups/groups.service.ts index 1cdd80be..f4ec9697 100644 --- a/src/@seed/api/groups/groups.service.ts +++ b/src/@seed/api/groups/groups.service.ts @@ -92,4 +92,20 @@ export class GroupsService { }), ) } + + bulkUpdate(orgId: number, addGroupIds: number[], removeGroupIds: number[], viewIds: number[], type: 'property' | 'tax_lot'): Observable { + const url = `/api/v3/inventory_group_mappings/put/?organization_id=${orgId}` + const data = { + inventory_ids: viewIds, + add_group_ids: addGroupIds, + remove_group_ids: removeGroupIds, + inventory_type: type, + } + return this._httpClient.put(url, data).pipe( + tap(() => { this.list(orgId) }), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error updating groups') + }), + ) + } } diff --git a/src/@seed/api/organization/organization.service.ts b/src/@seed/api/organization/organization.service.ts index 9fb147a1..11ec334f 100644 --- a/src/@seed/api/organization/organization.service.ts +++ b/src/@seed/api/organization/organization.service.ts @@ -20,6 +20,7 @@ import type { CreateAccessLevelInstanceRequest, EditAccessLevelInstanceRequest, EditAccessLevelInstanceResponse, + FilterByViewsResponse, MatchingCriteriaColumnsResponse, Organization, OrganizationResponse, @@ -184,6 +185,17 @@ export class OrganizationService { ) } + filterAccessLevelsByViews(orgId: number, type: InventoryType, viewIds: number[]): Observable { + const url = `/api/v3/organizations/${orgId}/access_levels/filter_by_views/` + const data = { inventory_type: type, view_ids: viewIds } + return this._httpClient.post(url, data).pipe( + map(({ access_level_instance_ids }) => access_level_instance_ids), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error filtering access levels by views') + }), + ) + } + canDeleteAccessLevelInstance(organizationId: number, accessLevelInstanceId: number) { const url = `/api/v3/organizations/${organizationId}/access_levels/${accessLevelInstanceId}/can_delete_instance/` return this._httpClient.get(url).pipe( diff --git a/src/@seed/api/organization/organization.types.ts b/src/@seed/api/organization/organization.types.ts index beb125d4..9de0b73f 100644 --- a/src/@seed/api/organization/organization.types.ts +++ b/src/@seed/api/organization/organization.types.ts @@ -203,3 +203,8 @@ export type MatchingCriteriaColumnsResponse = { PropertyState: string[]; TaxLotState: string[]; } + +export type FilterByViewsResponse = { + access_level_instance_ids: number[]; + status: string; +} diff --git a/src/@seed/components/index.ts b/src/@seed/components/index.ts index 394ce3d0..8161d234 100644 --- a/src/@seed/components/index.ts +++ b/src/@seed/components/index.ts @@ -8,6 +8,7 @@ export * from './ag-grid' export * from './label' export * from './loading-bar' export * from './masonry' +export * from './menu' export * from './modal' export * from './navigation' export * from './not-found' diff --git a/src/@seed/components/menu/index.ts b/src/@seed/components/menu/index.ts new file mode 100644 index 00000000..b01e92af --- /dev/null +++ b/src/@seed/components/menu/index.ts @@ -0,0 +1 @@ +export * from './menu-item.component' diff --git a/src/@seed/components/menu/menu-item.component.html b/src/@seed/components/menu/menu-item.component.html new file mode 100644 index 00000000..8a7ce3aa --- /dev/null +++ b/src/@seed/components/menu/menu-item.component.html @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/src/@seed/components/menu/menu-item.component.ts b/src/@seed/components/menu/menu-item.component.ts new file mode 100644 index 00000000..4e9a924c --- /dev/null +++ b/src/@seed/components/menu/menu-item.component.ts @@ -0,0 +1,20 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core' +import { MaterialImports } from '@seed/materials' + +@Component({ + selector: 'seed-menu-item', + templateUrl: './menu-item.component.html', + imports: [MaterialImports], +}) +export class MenuItemComponent { + @Input() label = '' + @Input() icon?: string + @Input() disabled = false + @Output() action = new EventEmitter() + + onClick() { + if (!this.disabled) { + this.action.emit() + } + } +} diff --git a/src/app/modules/inventory-list/list/actions/groups-modal.component.html b/src/app/modules/inventory-list/list/actions/groups-modal.component.html new file mode 100644 index 00000000..60459c78 --- /dev/null +++ b/src/app/modules/inventory-list/list/actions/groups-modal.component.html @@ -0,0 +1,59 @@ + +@if (loading) { +
+ +
+} +@else if (allSameAli) { + + + + + +
+ + Not seeing the right group? Create a new one. +
+ + Group Name + + @if (form.controls.name?.hasError('valueExists')) { + This name already exists. + } + + + +
+
+ +} @else { + + + Selection includes multiple Access Level Instances. To update or create a group, all properties must be in the same access level instance. Modify selection and try again. + +} + +
+ + +
\ No newline at end of file diff --git a/src/app/modules/inventory-list/list/actions/groups-modal.component.ts b/src/app/modules/inventory-list/list/actions/groups-modal.component.ts new file mode 100644 index 00000000..f0b8fe55 --- /dev/null +++ b/src/app/modules/inventory-list/list/actions/groups-modal.component.ts @@ -0,0 +1,153 @@ +import { CommonModule } from '@angular/common' +import type { OnDestroy, OnInit } from '@angular/core' +import { Component, inject } from '@angular/core' +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms' +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' +import type { CurrentUser, InventoryGroup } from '@seed/api' +import { GroupsService, OrganizationService, UserService } from '@seed/api' +import { AlertComponent, ModalHeaderComponent } from '@seed/components' +import { MaterialImports } from '@seed/materials' +import { ConfigService } from '@seed/services' +import { SEEDValidators } from '@seed/validators' +import { AgGridAngular } from 'ag-grid-angular' +import type { CellValueChangedEvent, ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' +import type { InventoryDisplayType, InventoryType } from 'app/modules/inventory/inventory.types' +import { Subject, switchMap, take, takeUntil, tap } from 'rxjs' + +@Component({ + selector: 'seed-inventory-groups-modal', + templateUrl: './groups-modal.component.html', + imports: [AgGridAngular, + AlertComponent, + CommonModule, + FormsModule, + MaterialImports, + ModalHeaderComponent, + ReactiveFormsModule, + ], +}) +export class GroupsModalComponent implements OnDestroy, OnInit { + private _dialogRef = inject(MatDialogRef) + private _configService = inject(ConfigService) + private _groupsService = inject(GroupsService) + private _organizationService = inject(OrganizationService) + private _userService = inject(UserService) + private readonly _unsubscribeAll$ = new Subject() + + aliIds: number[] = [] + aliId: number + // aliIds: number[] = [] + currentUser: CurrentUser + gridTheme$ = this._configService.gridTheme$ + groups: InventoryGroup[] = [] + aliGroups: (InventoryGroup & { add: boolean; remove: boolean })[] = [] + gridApi: GridApi + columnDefs: ColDef[] = [] + allSameAli = true + loading = true + + data = inject(MAT_DIALOG_DATA) as { orgId: number; type: InventoryType; viewIds: number[]; existingGroupNames: string[] } + + form = new FormGroup({ + name: new FormControl('', [ + Validators.required, + SEEDValidators.uniqueValue(this.data.existingGroupNames), + ]), + organization: new FormControl(this.data.orgId), + inventory_type: new FormControl(this.data.type === 'taxlots' ? 'Tax Lot' : 'Property'), + access_level_instance: new FormControl(null, Validators.required), + }) + + ngOnInit(): void { + const { orgId, type, viewIds } = this.data + this._groupsService.list(orgId) + this._organizationService.filterAccessLevelsByViews(orgId, type, viewIds) + .pipe( + tap((aliIds) => { + this.aliIds = aliIds + this.setGrid() + }), + switchMap(() => this._groupsService.groups$), + tap((groups) => { this.setGroups(groups) }), + takeUntil(this._unsubscribeAll$), + ).subscribe() + } + + setGroups(groups: InventoryGroup[]) { + this.groups = groups + this.aliGroups = this.groups + .filter((g) => this.aliIds.includes(g.access_level_instance)) + .map((group) => ({ ...group, add: false, remove: false })) + this.aliId = this.aliGroups[0]?.access_level_instance + this.allSameAli = this.aliGroups.every((g) => g.access_level_instance === this.aliId) + if (this.allSameAli) { + this.form.patchValue({ access_level_instance: this.aliId }) + } + this.loading = false + } + + setGrid() { + this.setRowData() + this.columnDefs = [ + { field: 'name', headerName: 'Group Name' }, + { field: 'access_level_instance_data.name', headerName: 'Access Level Instance' }, + { field: 'inventory_list', headerName: 'Inventory', flex: 0.5, valueFormatter: ({ data }: { data: InventoryGroup }) => String(data.inventory_list.length) }, + { field: 'add', headerName: 'Add', flex: 0.5, editable: this.allSameAli, headerClass: () => this.allSameAli ? '' : 'text-secondary' }, + { field: 'remove', headerName: 'Remove', flex: 0.5, editable: true }, + ] + } + + setRowData() { + this.aliGroups = this.groups + .filter((g) => this.aliIds.includes(g.access_level_instance)) + .map((group) => ({ ...group, add: false, remove: false })) + } + + onCellValueChanged(event: CellValueChangedEvent): void { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { colDef, newValue, node } = event + const field = colDef.field + const otherField = field === 'add' ? 'remove' : 'add' + const data = node.data as InventoryGroup & { add: boolean; remove: boolean } + + if (newValue && data[otherField]) { + node.setDataValue(otherField, false) + } + } + + onGridReady(agGrid: GridReadyEvent) { + this.gridApi = agGrid.api + this.gridApi.autoSizeAllColumns() + } + + onSubmit() { + this._groupsService.create(this.data.orgId, this.form.value as unknown as InventoryGroup) + .pipe(take(1)) + .subscribe() + } + + done() { + const { orgId, viewIds, type } = this.data + const groupType = type === 'taxlots' ? 'tax_lot' : 'property' + const addGroupIds: number[] = this.aliGroups.filter((g) => g.add).map((g) => g.id) + const removeGroupIds: number[] = this.aliGroups.filter((g) => g.remove).map((g) => g.id) + + if (!addGroupIds.length && !removeGroupIds.length) { + this.close() + return + } + + this._groupsService.bulkUpdate(orgId, addGroupIds, removeGroupIds, viewIds, groupType).subscribe(() => { + this.close(true) + }) + } + + close(success = false) { + this._dialogRef.close(success) + } + + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } +} diff --git a/src/app/modules/inventory-list/list/actions/index.ts b/src/app/modules/inventory-list/list/actions/index.ts new file mode 100644 index 00000000..14cbe50e --- /dev/null +++ b/src/app/modules/inventory-list/list/actions/index.ts @@ -0,0 +1 @@ +export * from './groups-modal.component' diff --git a/src/app/modules/inventory-list/list/grid/actions.component.html b/src/app/modules/inventory-list/list/grid/actions.component.html index 3fe8388d..d853c8ec 100644 --- a/src/app/modules/inventory-list/list/grid/actions.component.html +++ b/src/app/modules/inventory-list/list/grid/actions.component.html @@ -1,14 +1,192 @@ -
- Actions - - @for (item of actions; track $index) { - {{ item.name }} - } - + +
+ Actions +
+
Select Action
+ +
+ + + + + + + + + + + Access Levels +
+ + + + +
+ + + Analyses +
+ + +
+ + + Audit Template +
+ + + + +
+ + + + + Data Quality +
+ + +
+ + + Derived Data +
+ + +
+ + + + + + + + + + Groups +
+ + +
+ + + Labels +
+ + +
+ + + + + + + Salesforce +
+ + +
+ + + UBID +
+ + + + + + +
+ +
\ No newline at end of file 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 3faaa376..4f22b892 100644 --- a/src/app/modules/inventory-list/list/grid/actions.component.ts +++ b/src/app/modules/inventory-list/list/grid/actions.component.ts @@ -1,22 +1,21 @@ -import type { OnDestroy } from '@angular/core' +import type { OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core' import { Component, EventEmitter, inject, Input, Output } from '@angular/core' import { MatDialog } from '@angular/material/dialog' -import { type MatSelect } from '@angular/material/select' import type { GridApi } from 'ag-grid-community' import { filter, Subject, switchMap, takeUntil, tap } from 'rxjs' -import { InventoryService } from '@seed/api' -import { DeleteModalComponent } from '@seed/components' +import { GroupsService, InventoryService } from '@seed/api' +import { DeleteModalComponent, MenuItemComponent } from '@seed/components' import { MaterialImports } from '@seed/materials' import { ModalComponent } from 'app/modules/column-list-profile/modal/modal.component' import type { InventoryType, Profile } from '../../../inventory/inventory.types' -import { MoreActionsModalComponent } from '../modal' +import { GroupsModalComponent } from '../actions' @Component({ selector: 'seed-inventory-grid-actions', templateUrl: './actions.component.html', - imports: [DeleteModalComponent, MaterialImports], + imports: [MenuItemComponent, DeleteModalComponent, MaterialImports], }) -export class ActionsComponent implements OnDestroy { +export class ActionsComponent implements OnDestroy, OnChanges, OnInit { @Input() cycleId: number @Input() gridApi: GridApi @Input() inventory: Record[] @@ -28,65 +27,28 @@ export class ActionsComponent implements OnDestroy { @Output() refreshInventory = new EventEmitter() @Output() selectedAll = new EventEmitter() private _inventoryService = inject(InventoryService) + private _groupsService = inject(GroupsService) private _dialog = inject(MatDialog) private readonly _unsubscribeAll$ = new Subject() + hasSelection: boolean + existingGroupNames: string[] = [] - get actions() { - return [ - { - name: 'Select All', - action: () => { - this.selectAll() - }, - disabled: false, - }, - { - name: 'Select None', - action: () => { - this.deselectAll() - }, - disabled: false, - }, - { - name: 'Only Show Populated Columns', - action: () => { - this.openShowPopulatedColumnsModal() - }, - disabled: !this.inventory, - }, - { name: 'Delete', action: this.deleteStates, disabled: !this.selectedViewIds.length }, - { name: 'Merge', action: this.tempAction, disabled: !this.selectedViewIds.length }, - { - name: 'More...', - action: () => { - this.openMoreActionsModal() - }, - disabled: !this.selectedViewIds.length, - }, - ] + ngOnInit(): void { + this._groupsService.list(this.orgId) + this._groupsService.groups$.pipe( + takeUntil(this._unsubscribeAll$), + tap((groups) => { this.existingGroupNames = groups.map((g) => g.name) }), + ).subscribe() } - tempAction() { - console.log('temp action') - } - - openMoreActionsModal() { - 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() + ngOnChanges(changes: SimpleChanges): void { + if (changes.selectedViewIds) { + this.hasSelection = this.selectedViewIds.length > 0 + } } - onAction(action: () => void, select: MatSelect) { - action() - select.value = null + tempAction() { + console.log('temp action') } selectAll() { @@ -157,6 +119,23 @@ export class ActionsComponent implements OnDestroy { }) } + openGroupsModal() { + const dialogRef = this._dialog.open(GroupsModalComponent, { + width: '50rem', + data: { + orgId: this.orgId, + type: this.type, + viewIds: this.selectedViewIds, + existingGroupNames: this.existingGroupNames, + }, + }) + + dialogRef.afterClosed().pipe( + filter(Boolean), + tap(() => { this.refreshInventory.emit() }), + ).subscribe() + } + ngOnDestroy(): void { this._unsubscribeAll$.next() this._unsubscribeAll$.complete() diff --git a/src/app/modules/inventory-list/list/grid/cell-header-menu.component.html b/src/app/modules/inventory-list/list/grid/cell-header-menu.component.html index 3000a815..f97e98ea 100644 --- a/src/app/modules/inventory-list/list/grid/cell-header-menu.component.html +++ b/src/app/modules/inventory-list/list/grid/cell-header-menu.component.html @@ -13,7 +13,7 @@ -
+
@@ -98,7 +98,30 @@
- + + + Groups +
+ + +
+ + + Labels +
+ + +
+ + + + Other - - - - Groups -
- - -
- - - Labels -
- - -
- - - Salesforce 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 e5a22c39..6632682d 100644 --- a/src/app/modules/inventory-list/list/grid/actions.component.ts +++ b/src/app/modules/inventory-list/list/grid/actions.component.ts @@ -39,6 +39,14 @@ export class ActionsComponent implements OnDestroy, OnChanges, OnInit { return } + baseData() { + return { + orgId: this.orgId, + type: this.type, + viewIds: this.selectedViewIds, + } + } + ngOnChanges(changes: SimpleChanges): void { if (changes.selectedViewIds) { this.hasSelection = this.selectedViewIds.length > 0 @@ -53,7 +61,7 @@ export class ActionsComponent implements OnDestroy, OnChanges, OnInit { this.gridApi.selectAll() const inventory_type = this.type === 'taxlots' ? 'taxlot' : 'property' const params = new URLSearchParams({ - cycle: this.cycleId.toString(), + cycle: this.cycleId?.toString(), ids_only: 'true', include_related: 'true', organization_id: this.orgId.toString(), @@ -116,36 +124,24 @@ export class ActionsComponent implements OnDestroy, OnChanges, OnInit { openAliChangeModal() { const dialogRef = this._dialog.open(AliChangeModalComponent, { width: '40rem', - data: { - orgId: this.orgId, - viewIds: this.selectedViewIds, - }, + data: this.baseData(), }) - this.afterClosed(dialogRef) } openAnalysisRunModal() { const dialogRef = this._dialog.open(AnalysisRunModalComponent, { width: '40rem', - data: { - orgId: this.orgId, - viewIds: this.selectedViewIds, - }, + data: this.baseData(), }) - this.afterClosed(dialogRef) } - startDataQualityCheck() { + openDataQualityCheck() { const dialogRef = this._dialog.open(DQCStartModalComponent, { width: '40rem', disableClose: true, - data: { - orgId: this.orgId, - type: this.type, - viewIds: this.selectedViewIds, - }, + data: this.baseData(), }) this.afterClosed(dialogRef) } @@ -153,11 +149,7 @@ export class ActionsComponent implements OnDestroy, OnChanges, OnInit { openDerivedDataUpdateModal() { const dialogRef = this._dialog.open(UpdateDerivedDataComponent, { width: '40rem', - data: { - orgId: this.orgId, - viewIds: this.selectedViewIds, - type: this.type, - }, + data: this.baseData(), }) this.afterClosed(dialogRef) } @@ -165,13 +157,16 @@ export class ActionsComponent implements OnDestroy, OnChanges, OnInit { openGroupsModal() { const dialogRef = this._dialog.open(GroupsModalComponent, { width: '50rem', - data: { - orgId: this.orgId, - type: this.type, - viewIds: this.selectedViewIds, - }, + data: this.baseData(), }) + this.afterClosed(dialogRef) + } + openLabelsModal() { + const dialogRef = this._dialog.open(GroupsModalComponent, { + width: '50rem', + data: this.baseData(), + }) this.afterClosed(dialogRef) } diff --git a/src/app/modules/inventory/actions/analysis-config/simple-config.component.ts b/src/app/modules/inventory/actions/analysis-config/simple-config.component.ts index 92c87f9e..1a63fa62 100644 --- a/src/app/modules/inventory/actions/analysis-config/simple-config.component.ts +++ b/src/app/modules/inventory/actions/analysis-config/simple-config.component.ts @@ -56,7 +56,7 @@ export class SimpleConfigComponent implements OnChanges, OnDestroy, OnInit { 'Element Statistics': this.formES, } this.form = this.formMap[this.service] - this.watchForm() + this.formChange.emit(this.form) } ngOnChanges(changes: SimpleChanges): void { diff --git a/src/app/modules/inventory/actions/index.ts b/src/app/modules/inventory/actions/index.ts index 8318b702..98a2b7fc 100644 --- a/src/app/modules/inventory/actions/index.ts +++ b/src/app/modules/inventory/actions/index.ts @@ -2,3 +2,4 @@ export * from './ali-change-modal.component' export * from './analysis-config' export * from './analysis-run-modal.component' export * from './groups-modal.component' +export * from './labels-modal.component' diff --git a/src/app/modules/inventory/actions/labels-modal.component.html b/src/app/modules/inventory/actions/labels-modal.component.html new file mode 100644 index 00000000..26915a78 --- /dev/null +++ b/src/app/modules/inventory/actions/labels-modal.component.html @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/src/app/modules/inventory/actions/labels-modal.component.ts b/src/app/modules/inventory/actions/labels-modal.component.ts new file mode 100644 index 00000000..6c0c8c45 --- /dev/null +++ b/src/app/modules/inventory/actions/labels-modal.component.ts @@ -0,0 +1,74 @@ +import { CommonModule } from '@angular/common' +import type { OnDestroy, OnInit} from '@angular/core' +import { Component, inject } from '@angular/core' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' +import type { Label} from '@seed/api' +import { LabelService } from '@seed/api' +import { ModalHeaderComponent } from '@seed/components' +import { MaterialImports } from '@seed/materials' +import { AgGridAngular } from 'ag-grid-angular' +import { Subject, takeUntil, tap } from 'rxjs' +import { InventoryType } from '../inventory.types' +import { CellValueChangedEvent, ColDef } from 'ag-grid-community' +import { ConfigService } from '@seed/services' + +@Component({ + selector: 'seed-labels-modal', + templateUrl: './labels-modal.component.html', + imports: [ + AgGridAngular, + CommonModule, + FormsModule, + MaterialImports, + ModalHeaderComponent, + ReactiveFormsModule, + ], +}) +export class LabelsModalComponent implements OnInit, OnDestroy { + private _unsubscribeAll$ = new Subject() + private _dialogRef = inject(MatDialogRef) + private _configService = inject(ConfigService) + private _labelService = inject(LabelService) + columnDefs: ColDef[] + labels: Label[] = [] + gridTheme$ = this._configService.gridTheme$ + + data = inject(MAT_DIALOG_DATA) as { orgId: number; type: InventoryType; viewIds: number[] } + + ngOnInit(): void { + this._labelService.labels$ + .pipe( + tap((labels) => { + this.labels = labels + this.setGrid() + }), + takeUntil(this._unsubscribeAll$), + ) + .subscribe() + } + + setGrid() { + this.columnDefs = [ + { field: 'name', headerName: 'Label', flex: 1 }, + { field: 'add', headerName: 'Add', flex: 0.5, editable: true }, + { field: 'remove', headerName: 'Remove', flex: 0.5, editable: true }, + ] + } + + onCellValueChanged(event: CellValueChangedEvent): void { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { colDef, newValue, node } = event + const field = colDef.field + const otherField = field === 'add' ? 'remove' : 'add' + const data = node.data as Label & { add: boolean; remove: boolean } + + if (newValue && data[otherField]) { + node.setDataValue(otherField, false) + } + } + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } +} \ No newline at end of file From 0507f040fe81ecef718066a01a01abb95f0e76f4 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Fri, 25 Jul 2025 19:41:55 +0000 Subject: [PATCH 11/16] label action --- src/@seed/api/label/label.service.ts | 24 +- src/@seed/api/label/label.types.ts | 4 +- .../components/menu/menu-item.component.html | 6 +- .../data-quality/results-modal.component.html | 2 +- .../data-quality/start-modal.component.html | 2 +- .../step1/map-data.component.html | 2 +- .../update-derived-data.component.html | 28 +- .../list/grid/actions.component.html | 280 +++++++----------- .../list/grid/actions.component.ts | 4 +- .../actions/ali-change-modal.component.html | 15 +- .../better-config.component.html | 50 ++-- .../analysis-config/bur-config.component.html | 51 ++-- .../simple-config.component.html | 75 +++-- .../actions/analysis-run-modal.component.html | 49 ++- .../actions/groups-modal.component.html | 39 +-- .../actions/labels-modal.component.html | 49 ++- .../actions/labels-modal.component.ts | 100 ++++++- .../labels/modal/form-modal.component.html | 3 + src/styles/styles.scss | 14 +- 19 files changed, 401 insertions(+), 396 deletions(-) diff --git a/src/@seed/api/label/label.service.ts b/src/@seed/api/label/label.service.ts index 24237e3d..b0bdfc28 100644 --- a/src/@seed/api/label/label.service.ts +++ b/src/@seed/api/label/label.service.ts @@ -2,11 +2,11 @@ import type { HttpErrorResponse, HttpResponse } 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, switchMap } from 'rxjs' +import { catchError, map, ReplaySubject, switchMap, tap } from 'rxjs' import { ErrorService } from '@seed/services' import { naturalSort } from '@seed/utils' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' -import type { InventoryType } from 'app/modules/inventory/inventory.types' +import type { InventoryType, InventoryTypeSingular } from 'app/modules/inventory/inventory.types' import { UserService } from '../user' import type { Label } from './label.types' @@ -81,10 +81,7 @@ export class LabelService { create(label: Label): Observable
\ No newline at end of file +
diff --git a/src/app/modules/data-quality/results-modal.component.html b/src/app/modules/data-quality/results-modal.component.html index 4fa659a4..8cf29cd4 100644 --- a/src/app/modules/data-quality/results-modal.component.html +++ b/src/app/modules/data-quality/results-modal.component.html @@ -13,7 +13,7 @@ [pagination]="true" [paginationPageSize]="10" [paginationPageSizeSelector]="[10, 50, 100]" - > + > } @else {
No warnings or errors
diff --git a/src/app/modules/data-quality/start-modal.component.html b/src/app/modules/data-quality/start-modal.component.html index 7bbdfe39..b464fc14 100644 --- a/src/app/modules/data-quality/start-modal.component.html +++ b/src/app/modules/data-quality/start-modal.component.html @@ -2,4 +2,4 @@ [total]="progressBarObj.total" [progress]="progressBarObj.progress" [title]="progressBarObj.statusMessage || 'Running Data Quality Check...'" -> \ No newline at end of file +> 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 0ce7d96e..e8806a30 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 @@ -34,10 +34,10 @@
Properties diff --git a/src/app/modules/inventory-list/list/actions/update-derived-data.component.html b/src/app/modules/inventory-list/list/actions/update-derived-data.component.html index 700eb792..031b6267 100644 --- a/src/app/modules/inventory-list/list/actions/update-derived-data.component.html +++ b/src/app/modules/inventory-list/list/actions/update-derived-data.component.html @@ -1,17 +1,14 @@ - + - +
- This process recalculates the data stored in derived columns for the - selected properties and tax lots. This may take several minutes. - + This process recalculates the data stored in derived columns for the selected properties and tax lots. This may take several + minutes. +
- + - - Access Levels -
- @if (type === 'properties') { - - - } - - -
- + + + + + + + + + + Access Levels +
@if (type === 'properties') { - - Analyses -
- - -
+ } - + +
+ + @if (type === 'properties') { - Audit Template + Analyses
- - - - -
- - - - - Data Quality -
- - -
- - - Derived Data -
- - -
- - - - Groups -
- - -
- - - Labels -
- - -
- - - - Other - - - - - - - - Salesforce -
- - -
- - - UBID -
- - - - - - +
+ } + + + Audit Template +
+ + +
+ -
\ No newline at end of file + + + Data Quality +
+ +
+ + + Derived Data +
+ +
+ + + + Groups +
+ +
+ + + Labels +
+ +
+ + + + Other + + + + + + + + Salesforce +
+ +
+ + + UBID +
+ + + + + +
+ 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 6632682d..4b7c0716 100644 --- a/src/app/modules/inventory-list/list/grid/actions.component.ts +++ b/src/app/modules/inventory-list/list/grid/actions.component.ts @@ -9,7 +9,7 @@ import { DeleteModalComponent, MenuItemComponent } from '@seed/components' import { MaterialImports } from '@seed/materials' import { ModalComponent } from 'app/modules/column-list-profile/modal/modal.component' import { DQCStartModalComponent } from 'app/modules/data-quality' -import { AliChangeModalComponent, AnalysisRunModalComponent, GroupsModalComponent } from 'app/modules/inventory/actions' +import { AliChangeModalComponent, AnalysisRunModalComponent, GroupsModalComponent, LabelsModalComponent } from 'app/modules/inventory/actions' import type { InventoryType, Profile } from '../../../inventory/inventory.types' import { UpdateDerivedDataComponent } from '../actions' @@ -163,7 +163,7 @@ export class ActionsComponent implements OnDestroy, OnChanges, OnInit { } openLabelsModal() { - const dialogRef = this._dialog.open(GroupsModalComponent, { + const dialogRef = this._dialog.open(LabelsModalComponent, { width: '50rem', data: this.baseData(), }) diff --git a/src/app/modules/inventory/actions/ali-change-modal.component.html b/src/app/modules/inventory/actions/ali-change-modal.component.html index f54f82da..13b228bc 100644 --- a/src/app/modules/inventory/actions/ali-change-modal.component.html +++ b/src/app/modules/inventory/actions/ali-change-modal.component.html @@ -1,10 +1,8 @@ - +
- Please select an Access Level and Access Level Instance to move {{ data.viewIds.length }} {{ data.viewIds.length === 1 ? 'property' : 'properties'}} to. + Please select an Access Level and Access Level Instance to move {{ data.viewIds.length }} + {{ data.viewIds.length === 1 ? 'property' : 'properties' }} to.
@@ -26,7 +24,6 @@
-
- - -
\ No newline at end of file +
+ +
diff --git a/src/app/modules/inventory/actions/analysis-config/better-config.component.html b/src/app/modules/inventory/actions/analysis-config/better-config.component.html index d8228680..649ed6a3 100644 --- a/src/app/modules/inventory/actions/analysis-config/better-config.component.html +++ b/src/app/modules/inventory/actions/analysis-config/better-config.component.html @@ -1,50 +1,44 @@ -
- -
- The BETTER analysis leverages better.lbl.gov to calculate energy, cost, and GHG emission savings by comparing the - property's change point model with a benchmarked model. The results include saving potential and a list of recommended - high-level energy conservation measures. + +
+ The BETTER analysis leverages better.lbl.gov to calculate energy, cost, and GHG emission savings by comparing the property's change + point model with a benchmarked model. The results include saving potential and a list of recommended high-level energy conservation + measures.
- + Savings Target @for (option of savingsTargets; track option) { - {{ option }} + {{ option }} } - - + + Benchmark Data Type @for (option of benchmarkDataTypes; track option) { - {{ option }} + {{ option }} } - + Min Model R² - +
- -
- - Preprocess Meters - +
+ Preprocess Meters @if (viewIds.length > 1) { - - Portfolio Analysis - + Portfolio Analysis }
- + Cycle Meter Data Range All Meter Data @@ -60,18 +54,18 @@ } @else if (form.value.select_meters === 'date_range') { -
+
Start Date - - + + End Date - - + + @if (form.get('meter.end_date')?.hasError('dateBefore')) { End date must be after start date. @@ -79,4 +73,4 @@
} - \ No newline at end of file + diff --git a/src/app/modules/inventory/actions/analysis-config/bur-config.component.html b/src/app/modules/inventory/actions/analysis-config/bur-config.component.html index 24fb4f59..6877cb56 100644 --- a/src/app/modules/inventory/actions/analysis-config/bur-config.component.html +++ b/src/app/modules/inventory/actions/analysis-config/bur-config.component.html @@ -1,34 +1,35 @@ -
- -
- The Building Upgrade Recommendation analysis implements a workflow to identify buildings that may need a deep energy retrofit, equipment replaced or re-tuning based on building attributes such as energy use, year built, and square footage. If your organization contains elements, the Element Statistics Analysis should be run prior to running this analysis. + +
+ The Building Upgrade Recommendation analysis implements a workflow to identify buildings that may need a deep energy retrofit, equipment + replaced or re-tuning based on building attributes such as energy use, year built, and square footage. If your organization contains + elements, the Element Statistics Analysis should be run prior to running this analysis.
@for (field of fields; track field.name) { - - {{ field.label }} + + {{ field.label }} - @if (field.type === 'select') { - - @for (col of field.options; track $index) { - {{ col.display_name }} + @if (field.type === 'select') { + + @for (col of field.options; track $index) { + {{ col.display_name }} + } + + } @else { + } - - } @else { - - } - @if (field.hint) { - {{ field.hint }} - } - + @if (field.hint) { + {{ field.hint }} + } + } - \ No newline at end of file + diff --git a/src/app/modules/inventory/actions/analysis-config/simple-config.component.html b/src/app/modules/inventory/actions/analysis-config/simple-config.component.html index e817b398..a9d51d2e 100644 --- a/src/app/modules/inventory/actions/analysis-config/simple-config.component.html +++ b/src/app/modules/inventory/actions/analysis-config/simple-config.component.html @@ -1,69 +1,62 @@ -
+
{{ aboutMap[service] }}
- -@if (service === 'BSyncr') { +@if (service === 'BSyncr') { -
+ BSyncr Model Selection @for (option of bsyncrModelOptions; track option) { - {{ option }} + {{ option }} }
} @else if (service === 'CO2') { - -
- - Save Results to Property - + + Save Results to Property
- } @else if (service === 'EUI') { - -
- + + Cycle Meter Data Range All Meter Data @if (form.value.select_meters === 'select_cycle') { - - Cycle - - @for (cycle of cycles; track cycle) { - {{ cycle.name }} - } - - - } @else if (form.value.select_meters === 'date_range') { -
- - Start Date - - - + + Cycle + + @for (cycle of cycles; track cycle) { + {{ cycle.name }} + } + + } @else if (form.value.select_meters === 'date_range') { +
+ + Start Date + + + + - - End Date - - - - @if (form.get('meter.end_date')?.hasError('dateBefore')) { - End date must be after start date. - } - -
+ + End Date + + + + @if (form.get('meter.end_date')?.hasError('dateBefore')) { + End date must be after start date. + } + +
} - -} \ No newline at end of file +} diff --git a/src/app/modules/inventory/actions/analysis-run-modal.component.html b/src/app/modules/inventory/actions/analysis-run-modal.component.html index 5beaf07a..df28148d 100644 --- a/src/app/modules/inventory/actions/analysis-run-modal.component.html +++ b/src/app/modules/inventory/actions/analysis-run-modal.component.html @@ -1,26 +1,21 @@ - + @if (!runningAnalysis) { -
-
+ Group Name - - @if (form.controls.name?.hasError('valueExists')) { - This name already exists. - } + + @if (form.controls.name?.hasError('valueExists')) { + This name already exists. + } Service @for (service of serviceTypes; track service) { - {{ service.display }} + {{ service.display }} } @@ -28,43 +23,35 @@ @if (service === 'BETTER') { } @else if (service === 'Building Upgrade Recommendation') { - + } @else if (service) { - + } @if (['BETTER', 'Building Upgrade Recommendation'].includes(service)) {
-
+
} -
} @else { } - -
- @if(!runningAnalysis) { - +
+ @if (!runningAnalysis) { + } - -
\ No newline at end of file +
diff --git a/src/app/modules/inventory/actions/groups-modal.component.html b/src/app/modules/inventory/actions/groups-modal.component.html index 04ad9740..6b814860 100644 --- a/src/app/modules/inventory/actions/groups-modal.component.html +++ b/src/app/modules/inventory/actions/groups-modal.component.html @@ -1,14 +1,9 @@ - + @if (loading) {
- +
-} -@else if (allSameAli) { - +} @else if (allSameAli) {
Not seeing the right group? Create a new one.
- - + + Group Name - + @if (form.controls.name?.hasError('valueExists')) { This name already exists. } - - +
- } @else { - - Selection includes multiple Access Level Instances. To update or create a group, all properties must be in the same access level instance. Modify selection and try again. - + Selection includes multiple Access Level Instances. To update or create a group, all properties must be in the same access level + instance. Modify selection and try again. } -
+
- -
\ No newline at end of file +
diff --git a/src/app/modules/inventory/actions/labels-modal.component.html b/src/app/modules/inventory/actions/labels-modal.component.html index 26915a78..6c96db60 100644 --- a/src/app/modules/inventory/actions/labels-modal.component.html +++ b/src/app/modules/inventory/actions/labels-modal.component.html @@ -1,15 +1,42 @@ - + \ No newline at end of file +> + +
+ +
Not seeing the right Label? Create a new one.
+
+ + Label Name + + + + + Color + + +
{{ form.controls.color?.value }}
+
+ @for (c of colors; track c) { + +
{{ c }}
+
+ } +
+
+ + Show in list + +
+
+ +
+ +
diff --git a/src/app/modules/inventory/actions/labels-modal.component.ts b/src/app/modules/inventory/actions/labels-modal.component.ts index 6c0c8c45..d614f12f 100644 --- a/src/app/modules/inventory/actions/labels-modal.component.ts +++ b/src/app/modules/inventory/actions/labels-modal.component.ts @@ -1,17 +1,18 @@ import { CommonModule } from '@angular/common' import type { OnDestroy, OnInit} from '@angular/core' import { Component, inject } from '@angular/core' -import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms' import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' -import type { Label} from '@seed/api' +import type { Label, LabelColor} from '@seed/api' import { LabelService } from '@seed/api' import { ModalHeaderComponent } from '@seed/components' import { MaterialImports } from '@seed/materials' import { AgGridAngular } from 'ag-grid-angular' -import { Subject, takeUntil, tap } from 'rxjs' +import { Subject, switchMap, takeUntil, tap } from 'rxjs' import { InventoryType } from '../inventory.types' import { CellValueChangedEvent, ColDef } from 'ag-grid-community' import { ConfigService } from '@seed/services' +import { SEEDValidators } from '@seed/validators' @Component({ selector: 'seed-labels-modal', @@ -30,17 +31,30 @@ export class LabelsModalComponent implements OnInit, OnDestroy { private _dialogRef = inject(MatDialogRef) private _configService = inject(ConfigService) private _labelService = inject(LabelService) + colors: LabelColor[] = ['red', 'orange', 'blue', 'light blue', 'green', 'gray'] columnDefs: ColDef[] - labels: Label[] = [] + existingNames: string[] = [] gridTheme$ = this._configService.gridTheme$ + gridHeight = 0 + labels: Label[] = [] + newLabel: Label + rowData: (Label & { add: boolean; remove: boolean })[] = [] data = inject(MAT_DIALOG_DATA) as { orgId: number; type: InventoryType; viewIds: number[] } + form = new FormGroup({ + organization_id: new FormControl(this.data.orgId), + name: new FormControl(null), + color: new FormControl('gray'), + show_in_list: new FormControl(true), + }) + ngOnInit(): void { this._labelService.labels$ .pipe( tap((labels) => { this.labels = labels + this.setValidator() this.setGrid() }), takeUntil(this._unsubscribeAll$), @@ -48,14 +62,49 @@ export class LabelsModalComponent implements OnInit, OnDestroy { .subscribe() } + setValidator() { + this.existingNames = this.labels.map((g) => g.name) + const nameCtrl = this.form.get('name') + nameCtrl?.setValidators([ + SEEDValidators.uniqueValue(this.existingNames), + ]) + } + setGrid() { + this.getGridHeight() + this.setColDefs() + this.setRowData() + } + + setRowData() { + this.rowData = this.labels.map((group) => ({ + ...group, + add: group.id === this.newLabel?.id, + remove: false, + })) + + this.newLabel = null + } + + setColDefs() { this.columnDefs = [ - { field: 'name', headerName: 'Label', flex: 1 }, - { field: 'add', headerName: 'Add', flex: 0.5, editable: true }, - { field: 'remove', headerName: 'Remove', flex: 0.5, editable: true }, + { + field: 'name', + headerName: 'Label', + flex: 1, + cellRenderer: this.labelRenderer, + }, + { field: 'add', headerName: 'Add', flex: 0.2, editable: true }, + { field: 'remove', headerName: 'Remove', flex: 0.2, editable: true }, ] } + labelRenderer({ data }: { data: Label }) { + return ` +
${data.name}
+ ` + } + onCellValueChanged(event: CellValueChangedEvent): void { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { colDef, newValue, node } = event @@ -67,8 +116,43 @@ export class LabelsModalComponent implements OnInit, OnDestroy { node.setDataValue(otherField, false) } } + + getGridHeight() { + this.gridHeight = Math.min(this.labels.length * 42 + 52, 500) + } + + onSubmit() { + const data = this.form.value as Label + this._labelService.create(data) + .pipe( + tap((label) => { this.newLabel = label }), + switchMap(() => this._labelService.getByOrgId(data.organization_id)), + tap(() => { this.form.reset() }), + ) + .subscribe() + } + + done() { + const { orgId, viewIds, type } = this.data + const addLabelIds: number[] = this.rowData.filter((g) => g.add).map((g) => g.id) + const removeLabelIds: number[] = this.rowData.filter((g) => g.remove).map((g) => g.id) + + if (!addLabelIds.length && !removeLabelIds.length) { + this.close() + return + } + + this._labelService.updateLabelInventory(orgId, viewIds, type, addLabelIds, removeLabelIds).subscribe(() => { + this.close(true) + }) + } + + close(success = false) { + this._dialogRef.close(success) + } + ngOnDestroy(): void { this._unsubscribeAll$.next() this._unsubscribeAll$.complete() } -} \ No newline at end of file +} diff --git a/src/app/modules/organizations/labels/modal/form-modal.component.html b/src/app/modules/organizations/labels/modal/form-modal.component.html index ce137067..3d11cd2b 100644 --- a/src/app/modules/organizations/labels/modal/form-modal.component.html +++ b/src/app/modules/organizations/labels/modal/form-modal.component.html @@ -15,6 +15,9 @@ Color + +
{{ form.controls.color?.value }}
+
@for (c of colors; track c) {
{{ c }}
Date: Mon, 28 Jul 2025 18:33:06 +0000 Subject: [PATCH 12/16] export modal fxnal --- src/@seed/api/inventory/inventory.service.ts | 23 +++- .../progress/progress-bar.component.html | 2 +- .../list/grid/actions.component.html | 1 + .../list/grid/actions.component.ts | 10 +- .../actions/export-modal.component.html | 54 +++++++++ .../actions/export-modal.component.ts | 114 ++++++++++++++++++ src/app/modules/inventory/actions/index.ts | 1 + src/app/modules/inventory/inventory.types.ts | 10 ++ 8 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 src/app/modules/inventory/actions/export-modal.component.html create mode 100644 src/app/modules/inventory/actions/export-modal.component.ts diff --git a/src/@seed/api/inventory/inventory.service.ts b/src/@seed/api/inventory/inventory.service.ts index 6bd15547..47538cde 100644 --- a/src/@seed/api/inventory/inventory.service.ts +++ b/src/@seed/api/inventory/inventory.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 { BehaviorSubject, catchError, map, tap, throwError } from 'rxjs' +import { BehaviorSubject, catchError, map, take, tap, throwError } from 'rxjs' import { ErrorService } from '@seed/services' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' import type { @@ -12,6 +12,7 @@ import type { GenericView, GenericViewsResponse, InventoryDisplayType, + InventoryExportData, InventoryType, InventoryTypeGoal, NewProfileData, @@ -350,4 +351,24 @@ export class InventoryService { }), ) } + + startInventoryExport(orgId: number): Observable { + const url = `/api/v3/tax_lot_properties/start_export/?organization_id=${orgId}` + return this._httpClient.get(url).pipe( + take(1), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error starting export') + }), + ) + } + + exportInventory(orgId: number, type: InventoryType, data: InventoryExportData): Observable { + const url = `/api/v3/tax_lot_properties/export/?inventory_type=${type}&organization_id=${orgId}` + return this._httpClient.post(url, data, { responseType: 'blob' }).pipe( + take(1), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error starting export') + }), + ) + } } diff --git a/src/@seed/components/progress/progress-bar.component.html b/src/@seed/components/progress/progress-bar.component.html index 1e31e892..2e02da7f 100644 --- a/src/@seed/components/progress/progress-bar.component.html +++ b/src/@seed/components/progress/progress-bar.component.html @@ -15,7 +15,7 @@
diff --git a/src/app/modules/inventory-list/list/grid/actions.component.html b/src/app/modules/inventory-list/list/grid/actions.component.html index b62dd674..26643ce4 100644 --- a/src/app/modules/inventory-list/list/grid/actions.component.html +++ b/src/app/modules/inventory-list/list/grid/actions.component.html @@ -23,6 +23,7 @@ icon="fa-solid:share-nodes" > + Access Levels 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 4b7c0716..a9678428 100644 --- a/src/app/modules/inventory-list/list/grid/actions.component.ts +++ b/src/app/modules/inventory-list/list/grid/actions.component.ts @@ -9,7 +9,7 @@ import { DeleteModalComponent, MenuItemComponent } from '@seed/components' import { MaterialImports } from '@seed/materials' import { ModalComponent } from 'app/modules/column-list-profile/modal/modal.component' import { DQCStartModalComponent } from 'app/modules/data-quality' -import { AliChangeModalComponent, AnalysisRunModalComponent, GroupsModalComponent, LabelsModalComponent } from 'app/modules/inventory/actions' +import { AliChangeModalComponent, AnalysisRunModalComponent, ExportModalComponent, GroupsModalComponent, LabelsModalComponent } from 'app/modules/inventory/actions' import type { InventoryType, Profile } from '../../../inventory/inventory.types' import { UpdateDerivedDataComponent } from '../actions' @@ -121,6 +121,14 @@ export class ActionsComponent implements OnDestroy, OnChanges, OnInit { this.afterClosed(dialogRef) } + openExportModal() { + const dialogRef = this._dialog.open(ExportModalComponent, { + width: '40rem', + data: { ...this.baseData(), profileId: this.profile?.id || null }, + }) + this.afterClosed(dialogRef) + } + openAliChangeModal() { const dialogRef = this._dialog.open(AliChangeModalComponent, { width: '40rem', diff --git a/src/app/modules/inventory/actions/export-modal.component.html b/src/app/modules/inventory/actions/export-modal.component.html new file mode 100644 index 00000000..344bba3a --- /dev/null +++ b/src/app/modules/inventory/actions/export-modal.component.html @@ -0,0 +1,54 @@ + + + +
+ + + +
+ + Name + + + + + CSV + BuildingSync (Excel) + GeoJSON + + + @if (form.value.export_type === 'geojson') { + Include Meter Readings (Only + recommended for small exports) + } + + @else if (form.value.export_type === 'csv') { + Include Label Header + } + + @else { +
+ } +
+ +
+ +
+
+ + + + + + +
+
+ diff --git a/src/app/modules/inventory/actions/export-modal.component.ts b/src/app/modules/inventory/actions/export-modal.component.ts new file mode 100644 index 00000000..86fbd81c --- /dev/null +++ b/src/app/modules/inventory/actions/export-modal.component.ts @@ -0,0 +1,114 @@ +import type { OnDestroy } from '@angular/core' +import { Component, inject, ViewChild } from '@angular/core' +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms' +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' +import type { MatStepper } from '@angular/material/stepper' +import { catchError, combineLatest, EMPTY, finalize, Subject, switchMap, takeUntil, tap } from 'rxjs' +import { InventoryService } from '@seed/api' +import { ModalHeaderComponent, ProgressBarComponent } from '@seed/components' +import { MaterialImports } from '@seed/materials' +import { UploaderService } from '@seed/services' +import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' +import type { InventoryExportData, InventoryType } from '../inventory.types' + +@Component({ + selector: 'seed-export-modal', + templateUrl: './export-modal.component.html', + imports: [FormsModule, MaterialImports, ModalHeaderComponent, ProgressBarComponent, ReactiveFormsModule], +}) +export class ExportModalComponent implements OnDestroy { + @ViewChild('stepper') stepper!: MatStepper + private _dialogRef = inject(MatDialogRef) + private _inventoryService = inject(InventoryService) + private _snackBar = inject(SnackBarService) + private _uploaderService = inject(UploaderService) + private readonly _unsubscribeAll$ = new Subject() + + progressBarObj = this._uploaderService.defaultProgressBarObj + filename: string + exportData: InventoryExportData + + data = inject(MAT_DIALOG_DATA) as { + orgId: number; + type: InventoryType; + viewIds: number[]; + profileId: number; + } + + form = new FormGroup({ + name: new FormControl(null, Validators.required), + include_notes: new FormControl(true), + export_type: new FormControl<'csv' | 'xlsx' | 'geojson'>('csv', Validators.required), + include_label_header: new FormControl(false), + include_meter_readings: new FormControl(false), + }) + + export() { + this._inventoryService.startInventoryExport(this.data.orgId) + .pipe( + tap(({ progress_key }) => { this.initExport(progress_key) }), + switchMap(() => this.pollExport()), + tap((response) => { this.downloadData(response[0]) }), + takeUntil(this._unsubscribeAll$), + catchError(() => { return EMPTY }), + finalize(() => { this.close() }), + ) + .subscribe() + } + + initExport(progress_key: string) { + this.stepper.next() + this.formatFilename() + this.formatExportData(this.filename, progress_key) + } + + pollExport() { + const { orgId, type } = this.data + return combineLatest([ + this._inventoryService.exportInventory(orgId, type, this.exportData), + this._uploaderService.checkProgressLoop({ + progressKey: this.exportData.progress_key, + progressBarObj: this.progressBarObj, + }), + ]) + } + + downloadData(data: Blob) { + const a = document.createElement('a') + const url = URL.createObjectURL(data) + a.href = url + a.download = this.exportData.filename + a.click() + URL.revokeObjectURL(url) + this._snackBar.success(`Exported ${this.exportData.filename}`) + } + + formatFilename() { + this.filename = this.form.value.name + const ext = `.${this.form.value.export_type}` + if (!this.filename.endsWith(ext)) { + this.filename += ext + } + } + + formatExportData(filename: string, progress_key: string) { + this.exportData = { + export_type: this.form.value.export_type, + filename, + ids: this.data.viewIds, + include_meter_readings: this.form.value.include_meter_readings, + include_notes: this.form.value.include_notes, + profile_id: this.data.profileId, + progress_key, + } + } + + close() { + this._dialogRef.close() + } + + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } +} diff --git a/src/app/modules/inventory/actions/index.ts b/src/app/modules/inventory/actions/index.ts index 98a2b7fc..867ca2c3 100644 --- a/src/app/modules/inventory/actions/index.ts +++ b/src/app/modules/inventory/actions/index.ts @@ -3,3 +3,4 @@ export * from './analysis-config' export * from './analysis-run-modal.component' export * from './groups-modal.component' export * from './labels-modal.component' +export * from './export-modal.component' diff --git a/src/app/modules/inventory/inventory.types.ts b/src/app/modules/inventory/inventory.types.ts index e6396557..2f39f5ed 100644 --- a/src/app/modules/inventory/inventory.types.ts +++ b/src/app/modules/inventory/inventory.types.ts @@ -255,3 +255,13 @@ export type PropertyDocumentType = 'application/pdf' | 'application/dxf' | 'text export type PropertyDocumentExtension = 'PDF' | 'DXF' | 'IDF' | 'OSM' export type CrossCyclesResponse = Record[]> + +export type InventoryExportData = { + export_type: 'csv' | 'xlsx' | 'geojson'; + filename: string; + ids: number[]; + include_meter_readings: boolean; + include_notes: boolean; + profile_id: number; + progress_key: string; +} From b75312634545eea77ae52a5500e10a92845b1ee1 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Mon, 28 Jul 2025 19:44:02 +0000 Subject: [PATCH 13/16] refresh metadata fxnal --- src/@seed/api/inventory/inventory.service.ts | 26 +++++++ .../inventory-list/list/actions/index.ts | 3 +- .../refresh-metadata-modal.component.html | 24 ++++++ .../refresh-metadata-modal.component.ts | 77 +++++++++++++++++++ .../list/grid/actions.component.html | 6 +- .../list/grid/actions.component.ts | 10 ++- 6 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.html create mode 100644 src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.ts diff --git a/src/@seed/api/inventory/inventory.service.ts b/src/@seed/api/inventory/inventory.service.ts index 47538cde..35733f00 100644 --- a/src/@seed/api/inventory/inventory.service.ts +++ b/src/@seed/api/inventory/inventory.service.ts @@ -371,4 +371,30 @@ export class InventoryService { }), ) } + + startRefreshMetadata(orgId: number): Observable { + const url = `/api/v3/tax_lot_properties/start_set_update_to_now/?organization_id=${orgId}` + return this._httpClient.get(url).pipe( + take(1), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error starting metadata refresh') + }), + ) + } + + refreshMetadata(orgId: number, propertyViews: number[], taxlotViews: number[], progressKey: string): Observable { + const url = '/api/v3/tax_lot_properties/set_update_to_now/' + const data = { + organization_id: orgId, + property_views: propertyViews, + taxlot_views: taxlotViews, + progress_key: progressKey, + } + return this._httpClient.post(url, data).pipe( + take(1), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error refreshing metadata') + }), + ) + } } diff --git a/src/app/modules/inventory-list/list/actions/index.ts b/src/app/modules/inventory-list/list/actions/index.ts index 1b8b159a..cf0e09bb 100644 --- a/src/app/modules/inventory-list/list/actions/index.ts +++ b/src/app/modules/inventory-list/list/actions/index.ts @@ -1 +1,2 @@ -export * from './update-derived-data.component' \ No newline at end of file +export * from './refresh-metadata-modal.component' +export * from './update-derived-data.component' diff --git a/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.html b/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.html new file mode 100644 index 00000000..dad2d904 --- /dev/null +++ b/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.html @@ -0,0 +1,24 @@ + + + +@if (!inProgress) { +
+ This will set the selected inventory's 'Updated' timestamp to {{ currentTime }}. +
+} + +@else { + +} + +
+ +
\ No newline at end of file diff --git a/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.ts b/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.ts new file mode 100644 index 00000000..b79ff833 --- /dev/null +++ b/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.ts @@ -0,0 +1,77 @@ +import type { OnDestroy, OnInit } from '@angular/core' +import { Component, inject } from '@angular/core' +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' +import { InventoryService } from '@seed/api' +import { ModalHeaderComponent, ProgressBarComponent } from '@seed/components' +import { MaterialImports } from '@seed/materials' +import { UploaderService } from '@seed/services' +import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' +import type { InventoryType } from 'app/modules/inventory/inventory.types' +import { combineLatest, filter, finalize, Subject, switchMap, takeUntil, tap } from 'rxjs' + +@Component({ + selector: 'seed-refresh-metadata-modal', + templateUrl: './refresh-metadata-modal.component.html', + imports: [MaterialImports, ModalHeaderComponent, ProgressBarComponent], +}) +export class RefreshMetadataModalComponent implements OnInit, OnDestroy { + private _dialogRef = inject(MatDialogRef) + private _inventoryService = inject(InventoryService) + private _snackBar = inject(SnackBarService) + private _uploaderService = inject(UploaderService) + private _unsubscribeAll$ = new Subject() + currentTime: string + inProgress = false + progressBarObj = this._uploaderService.defaultProgressBarObj + progressKey: string + + data = inject(MAT_DIALOG_DATA) as { + orgId: number; + viewIds: number[]; + type: InventoryType; + } + + ngOnInit() { + setInterval(() => { + this.currentTime = new Date().toLocaleTimeString() + }, 1000) + } + + refresh() { + this.inProgress = true + this._inventoryService.startRefreshMetadata(this.data.orgId) + .pipe( + switchMap(({ progress_key }) => this.pollRefresh(progress_key)), + filter(([_, progressResponse]) => progressResponse.progress >= 100), + tap(() => { + this._snackBar.success('Success') + this.close(true) + }), + takeUntil(this._unsubscribeAll$), + finalize(() => { this.inProgress = false }), + ) + .subscribe() + } + + pollRefresh(progress_key: string) { + const { orgId, type, viewIds } = this.data + const [propertyViews, taxlotViews] = type == 'taxlots' ? [[], viewIds] : [viewIds, []] + + return combineLatest([ + this._inventoryService.refreshMetadata(orgId, propertyViews, taxlotViews, progress_key), + this._uploaderService.checkProgressLoop({ + progressKey: progress_key, + progressBarObj: this.progressBarObj, + }), + ]) + } + + close(success = false): void { + this._dialogRef.close(success) + } + + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } +} diff --git a/src/app/modules/inventory-list/list/grid/actions.component.html b/src/app/modules/inventory-list/list/grid/actions.component.html index 26643ce4..fd05901e 100644 --- a/src/app/modules/inventory-list/list/grid/actions.component.html +++ b/src/app/modules/inventory-list/list/grid/actions.component.html @@ -80,7 +80,6 @@ Other - 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 a9678428..8495f964 100644 --- a/src/app/modules/inventory-list/list/grid/actions.component.ts +++ b/src/app/modules/inventory-list/list/grid/actions.component.ts @@ -11,7 +11,7 @@ import { ModalComponent } from 'app/modules/column-list-profile/modal/modal.comp import { DQCStartModalComponent } from 'app/modules/data-quality' import { AliChangeModalComponent, AnalysisRunModalComponent, ExportModalComponent, GroupsModalComponent, LabelsModalComponent } from 'app/modules/inventory/actions' import type { InventoryType, Profile } from '../../../inventory/inventory.types' -import { UpdateDerivedDataComponent } from '../actions' +import { RefreshMetadataModalComponent, UpdateDerivedDataComponent } from '../actions' @Component({ selector: 'seed-inventory-grid-actions', @@ -178,6 +178,14 @@ export class ActionsComponent implements OnDestroy, OnChanges, OnInit { this.afterClosed(dialogRef) } + openRefreshMetadataModal() { + const dialogRef = this._dialog.open(RefreshMetadataModalComponent, { + width: '40rem', + data: this.baseData(), + }) + this.afterClosed(dialogRef) + } + afterClosed(dialogRef: MatDialogRef) { dialogRef.afterClosed().pipe( filter(Boolean), From e0e5b2b61558a70535ba0306cab3b63208f63421 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Tue, 29 Jul 2025 17:21:37 +0000 Subject: [PATCH 14/16] geocode in progress --- src/@seed/api/geocode/geocode.service.ts | 72 ++++++++++++++++ src/@seed/api/geocode/geocode.types.ts | 18 ++++ src/@seed/api/geocode/index.ts | 2 + src/@seed/api/index.ts | 1 + .../list/actions/geocode-modal.component.html | 40 +++++++++ .../list/actions/geocode-modal.component.ts | 84 +++++++++++++++++++ .../inventory-list/list/actions/index.ts | 1 + .../list/grid/actions.component.html | 2 +- .../list/grid/actions.component.ts | 10 ++- 9 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 src/@seed/api/geocode/geocode.service.ts create mode 100644 src/@seed/api/geocode/geocode.types.ts create mode 100644 src/@seed/api/geocode/index.ts create mode 100644 src/app/modules/inventory-list/list/actions/geocode-modal.component.html create mode 100644 src/app/modules/inventory-list/list/actions/geocode-modal.component.ts diff --git a/src/@seed/api/geocode/geocode.service.ts b/src/@seed/api/geocode/geocode.service.ts new file mode 100644 index 00000000..f2da24a6 --- /dev/null +++ b/src/@seed/api/geocode/geocode.service.ts @@ -0,0 +1,72 @@ +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 type { InventoryType } from 'app/modules/inventory' +import type { Observable } from 'rxjs' +import { catchError } from 'rxjs' +import type { ConfidenceSummary, GeocodingColumns } from './geocode.types' + +@Injectable({ providedIn: 'root' }) +export class GeocodeService { + private _httpClient = inject(HttpClient) + private _errorService = inject(ErrorService) + + geocode(orgId: number, viewIds: number[], type: InventoryType): Observable { + const url = `/api/v3/geocode/geocode_by_ids/&organization_id=${orgId}` + const data = { + property_view_ids: type === 'taxlots' ? [] : viewIds, + taxlot_view_ids: type === 'taxlots' ? viewIds : [], + } + return this._httpClient.post(url, data) + .pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Geocode Error') + }), + ) + } + + confidenceSummary(orgId: number, viewIds: number[], type: InventoryType): Observable { + const url = `/api/v3/geocode/confidence_summary/?organization_id=${orgId}` + const data = { + property_view_ids: type === 'taxlots' ? [] : viewIds, + taxlot_view_ids: type === 'taxlots' ? viewIds : [], + } + return this._httpClient.post(url, data) + .pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Geocode Confidence Summary Error') + }), + ) + } + + checkApiKey(orgId: number): Observable { + const url = `/api/v3/organizations/${orgId}/geocode_api_key_exists/` + return this._httpClient.get(url) + .pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Geocode API Key Check Error') + }), + ) + } + + geocodingEnabled(orgId: number): Observable { + const url = `/api/v3/organizations/${orgId}/geocoding_enabled/` + return this._httpClient.get(url) + .pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Geocoding Enabled Check Error') + }), + ) + } + + geocodingColumns(orgId: number): Observable { + const url = `/api/v3/organizations/${orgId}/geocoding_columns/` + return this._httpClient.get(url) + .pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Geocoding Columns Error') + }), + ) + } +} diff --git a/src/@seed/api/geocode/geocode.types.ts b/src/@seed/api/geocode/geocode.types.ts new file mode 100644 index 00000000..a9c3206b --- /dev/null +++ b/src/@seed/api/geocode/geocode.types.ts @@ -0,0 +1,18 @@ +export type ConfidenceSummary = { + properties: InventoryConfidenceSummary; + taxlots: InventoryConfidenceSummary; +} + +export type InventoryConfidenceSummary = { + census_geocoder: number; + high_confidence: number; + low_confidence: number; + manual: number; + missing_address_components: number; + not_geocoded: number; +} + +export type GeocodingColumns = { + PropertyState: string[]; // column_name + TaxLotState: string[]; +} diff --git a/src/@seed/api/geocode/index.ts b/src/@seed/api/geocode/index.ts new file mode 100644 index 00000000..6ec3e9a2 --- /dev/null +++ b/src/@seed/api/geocode/index.ts @@ -0,0 +1,2 @@ +export * from './geocode.service' +export * from './geocode.types' diff --git a/src/@seed/api/index.ts b/src/@seed/api/index.ts index bfa115f2..a7a17037 100644 --- a/src/@seed/api/index.ts +++ b/src/@seed/api/index.ts @@ -7,6 +7,7 @@ export * from './cycle' export * from './data-quality' export * from './dataset' export * from './derived-column' +export * from './geocode' export * from './groups' export * from './inventory' export * from './label' diff --git a/src/app/modules/inventory-list/list/actions/geocode-modal.component.html b/src/app/modules/inventory-list/list/actions/geocode-modal.component.html new file mode 100644 index 00000000..adba7826 --- /dev/null +++ b/src/app/modules/inventory-list/list/actions/geocode-modal.component.html @@ -0,0 +1,40 @@ + + + + +
+ @if (!hasApiKey) { + +
+ {{ t('NO_MAPQUEST_API_KEY_FOR_ORG') }} +
+
+ {{ t('DIRECTIONS_FOR_UPDATING_MQ_API_KEY') }} +
+
+ } + + @if (!geocodingEnabled) { + + {{ t('Geocoding has been disabled for this organization.') }} + + } + + + + + + +
+ +
+ +
\ No newline at end of file diff --git a/src/app/modules/inventory-list/list/actions/geocode-modal.component.ts b/src/app/modules/inventory-list/list/actions/geocode-modal.component.ts new file mode 100644 index 00000000..a21c5b19 --- /dev/null +++ b/src/app/modules/inventory-list/list/actions/geocode-modal.component.ts @@ -0,0 +1,84 @@ +import type { OnDestroy, OnInit} from '@angular/core' +import { Component, inject } from '@angular/core' +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' +import { GeocodeService } from '@seed/api' +import type { ConfidenceSummary, GeocodingColumns } from '@seed/api/geocode/geocode.types' +import { AlertComponent, ModalHeaderComponent } from '@seed/components' +import { SharedImports } from '@seed/directives' +import { MaterialImports } from '@seed/materials' +import type { InventoryType } from 'app/modules/inventory/inventory.types' +import { forkJoin, Subject, tap } from 'rxjs' + +@Component({ + selector: 'seed-geocode-modal', + templateUrl: './geocode-modal.component.html', + imports: [AlertComponent, MaterialImports, ModalHeaderComponent, SharedImports], +}) +export class GeocodeModalComponent implements OnInit, OnDestroy { + private _dialogRef = inject(MatDialogRef) + private _geocodeService = inject(GeocodeService) + private _unsubscribeAll$ = new Subject() + + confidenceSummary: ConfidenceSummary + geocodingEnabled = true + hasApiKey = true + hasEnoughGeoCols = true + hasGeoColumns = true + suggestVerify = true + notGeocoded = false + + geocodeState: 'verify' | 'geocode' | 'result' | 'fail' = 'verify' + + data = inject(MAT_DIALOG_DATA) as { + orgId: number; + viewIds: number[]; + type: InventoryType; + } + + get valid() { + return this.hasApiKey && this.geocodingEnabled && this.hasGeoColumns + } + + ngOnInit(): void { + this.getGeocodeConfig() + } + + getGeocodeConfig() { + forkJoin([ + this._geocodeService.checkApiKey(this.data.orgId), + this._geocodeService.geocodingEnabled(this.data.orgId), + this._geocodeService.geocodingColumns(this.data.orgId), + this._geocodeService.confidenceSummary(this.data.orgId, this.data.viewIds, this.data.type), + ]).pipe( + tap(([hasApiKey, geocodingEnabled, geoColumns, confidenceSummary]) => { + this.hasApiKey = hasApiKey + this.geocodingEnabled = geocodingEnabled + this.processGeoColumns(geoColumns) + this.processConfidenceSummary(confidenceSummary) + this.suggestVerify = hasApiKey && this.hasGeoColumns && this.geocodeState === 'verify' + }), + ).subscribe() + } + + processGeoColumns({ PropertyState, TaxLotState }: GeocodingColumns) { + this.hasGeoColumns = this.data.type === 'taxlots' ? TaxLotState.length > 0 : PropertyState.length > 0 + } + + processConfidenceSummary(confidenceSummary: ConfidenceSummary) { + this.confidenceSummary = confidenceSummary + // Process the confidence summary as needed + } + + close(success = false) { + this._dialogRef.close(success) + } + + onSubmit() { + console.log('submit') + } + + ngOnDestroy(): void { + this._unsubscribeAll$.next() + this._unsubscribeAll$.complete() + } +} diff --git a/src/app/modules/inventory-list/list/actions/index.ts b/src/app/modules/inventory-list/list/actions/index.ts index cf0e09bb..53534b5c 100644 --- a/src/app/modules/inventory-list/list/actions/index.ts +++ b/src/app/modules/inventory-list/list/actions/index.ts @@ -1,2 +1,3 @@ +export * from './geocode-modal.component' export * from './refresh-metadata-modal.component' export * from './update-derived-data.component' diff --git a/src/app/modules/inventory-list/list/grid/actions.component.html b/src/app/modules/inventory-list/list/grid/actions.component.html index fd05901e..4ef21c6f 100644 --- a/src/app/modules/inventory-list/list/grid/actions.component.html +++ b/src/app/modules/inventory-list/list/grid/actions.component.html @@ -86,7 +86,7 @@ (action)="tempAction()" label="FEMP CTS Export" > - + ) { dialogRef.afterClosed().pipe( filter(Boolean), From c04f206bdee0d5b1d1a67c67b17b7afbd391e6d3 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Tue, 29 Jul 2025 17:33:20 +0000 Subject: [PATCH 15/16] lint --- cspell.json | 1 + src/@seed/api/geocode/geocode.service.ts | 4 +-- .../progress/progress-bar.component.html | 6 +--- .../data-quality/start-modal.component.ts | 4 +-- .../list/actions/geocode-modal.component.html | 17 ++++------- .../list/actions/geocode-modal.component.ts | 4 +-- .../refresh-metadata-modal.component.html | 16 +++-------- .../refresh-metadata-modal.component.ts | 2 +- .../actions/update-derived-data.component.ts | 2 +- .../list/grid/actions.component.html | 6 +--- .../actions/ali-change-modal.component.ts | 2 +- .../analysis-config/bur-config.component.ts | 2 +- .../actions/export-modal.component.html | 28 +++++++------------ .../actions/groups-modal.component.ts | 6 ++-- .../actions/labels-modal.component.ts | 12 ++++---- src/styles/styles.scss | 6 ++-- 16 files changed, 44 insertions(+), 74 deletions(-) diff --git a/cspell.json b/cspell.json index c4e6b523..a9ae3264 100644 --- a/cspell.json +++ b/cspell.json @@ -25,6 +25,7 @@ "CEJST", "eeej", "EPSG", + "EISA", "FEMP", "falsey", "greenbutton", diff --git a/src/@seed/api/geocode/geocode.service.ts b/src/@seed/api/geocode/geocode.service.ts index f2da24a6..5e3fc35e 100644 --- a/src/@seed/api/geocode/geocode.service.ts +++ b/src/@seed/api/geocode/geocode.service.ts @@ -1,10 +1,10 @@ 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 type { InventoryType } from 'app/modules/inventory' import type { Observable } from 'rxjs' import { catchError } from 'rxjs' +import { ErrorService } from '@seed/services' +import type { InventoryType } from 'app/modules/inventory' import type { ConfidenceSummary, GeocodingColumns } from './geocode.types' @Injectable({ providedIn: 'root' }) diff --git a/src/@seed/components/progress/progress-bar.component.html b/src/@seed/components/progress/progress-bar.component.html index 2e02da7f..092ea6be 100644 --- a/src/@seed/components/progress/progress-bar.component.html +++ b/src/@seed/components/progress/progress-bar.component.html @@ -12,11 +12,7 @@ }
- +
@if (showSubProgress && subProgress && subProgress < 100) { diff --git a/src/app/modules/data-quality/start-modal.component.ts b/src/app/modules/data-quality/start-modal.component.ts index 069bb0d9..f0ed9c62 100644 --- a/src/app/modules/data-quality/start-modal.component.ts +++ b/src/app/modules/data-quality/start-modal.component.ts @@ -1,11 +1,11 @@ import type { OnDestroy, OnInit } from '@angular/core' import { Component, inject } from '@angular/core' import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog' +import { switchMap, takeUntil, tap } from 'rxjs' +import { Subject } from 'rxjs/internal/Subject' import { DataQualityService } from '@seed/api' import { ProgressBarComponent } from '@seed/components' import { UploaderService } from '@seed/services/uploader/uploader.service' -import { switchMap, takeUntil, tap } from 'rxjs' -import { Subject } from 'rxjs/internal/Subject' import type { InventoryType } from '../inventory' import { DQCResultsModalComponent } from './results-modal.component' diff --git a/src/app/modules/inventory-list/list/actions/geocode-modal.component.html b/src/app/modules/inventory-list/list/actions/geocode-modal.component.html index adba7826..225952bf 100644 --- a/src/app/modules/inventory-list/list/actions/geocode-modal.component.html +++ b/src/app/modules/inventory-list/list/actions/geocode-modal.component.html @@ -1,25 +1,20 @@ - - +
@if (!hasApiKey) { - +
{{ t('NO_MAPQUEST_API_KEY_FOR_ORG') }}
-
+
{{ t('DIRECTIONS_FOR_UPDATING_MQ_API_KEY') }}
} @if (!geocodingEnabled) { - + {{ t('Geocoding has been disabled for this organization.') }} } @@ -31,10 +26,8 @@ - -
-
\ No newline at end of file +
diff --git a/src/app/modules/inventory-list/list/actions/geocode-modal.component.ts b/src/app/modules/inventory-list/list/actions/geocode-modal.component.ts index a21c5b19..992204b2 100644 --- a/src/app/modules/inventory-list/list/actions/geocode-modal.component.ts +++ b/src/app/modules/inventory-list/list/actions/geocode-modal.component.ts @@ -1,13 +1,13 @@ -import type { OnDestroy, OnInit} from '@angular/core' +import type { OnDestroy, OnInit } from '@angular/core' import { Component, inject } from '@angular/core' import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' +import { forkJoin, Subject, tap } from 'rxjs' import { GeocodeService } from '@seed/api' import type { ConfidenceSummary, GeocodingColumns } from '@seed/api/geocode/geocode.types' import { AlertComponent, ModalHeaderComponent } from '@seed/components' import { SharedImports } from '@seed/directives' import { MaterialImports } from '@seed/materials' import type { InventoryType } from 'app/modules/inventory/inventory.types' -import { forkJoin, Subject, tap } from 'rxjs' @Component({ selector: 'seed-geocode-modal', diff --git a/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.html b/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.html index dad2d904..c847261c 100644 --- a/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.html +++ b/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.html @@ -1,17 +1,9 @@ - - + @if (!inProgress) { -
- This will set the selected inventory's 'Updated' timestamp to {{ currentTime }}. -
-} +
This will set the selected inventory's 'Updated' timestamp to {{ currentTime }}.
-@else { +} @else { -
\ No newline at end of file +
diff --git a/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.ts b/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.ts index b79ff833..c6fa0a18 100644 --- a/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.ts +++ b/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.ts @@ -1,13 +1,13 @@ import type { OnDestroy, OnInit } from '@angular/core' import { Component, inject } from '@angular/core' import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' +import { combineLatest, filter, finalize, Subject, switchMap, takeUntil, tap } from 'rxjs' import { InventoryService } from '@seed/api' import { ModalHeaderComponent, ProgressBarComponent } from '@seed/components' import { MaterialImports } from '@seed/materials' import { UploaderService } from '@seed/services' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' import type { InventoryType } from 'app/modules/inventory/inventory.types' -import { combineLatest, filter, finalize, Subject, switchMap, takeUntil, tap } from 'rxjs' @Component({ selector: 'seed-refresh-metadata-modal', diff --git a/src/app/modules/inventory-list/list/actions/update-derived-data.component.ts b/src/app/modules/inventory-list/list/actions/update-derived-data.component.ts index 77559051..80460b60 100644 --- a/src/app/modules/inventory-list/list/actions/update-derived-data.component.ts +++ b/src/app/modules/inventory-list/list/actions/update-derived-data.component.ts @@ -2,12 +2,12 @@ import type { OnDestroy, OnInit } from '@angular/core' import { Component, inject, ViewChild } from '@angular/core' import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' import type { MatStepper } from '@angular/material/stepper' +import { Subject, switchMap } from 'rxjs' import { InventoryService } from '@seed/api' import { ModalHeaderComponent, ProgressBarComponent } from '@seed/components' import { MaterialImports } from '@seed/materials' import { UploaderService } from '@seed/services' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' -import { Subject, switchMap } from 'rxjs' @Component({ selector: 'seed-update-derived-data', diff --git a/src/app/modules/inventory-list/list/grid/actions.component.html b/src/app/modules/inventory-list/list/grid/actions.component.html index 4ef21c6f..7efe75a4 100644 --- a/src/app/modules/inventory-list/list/grid/actions.component.html +++ b/src/app/modules/inventory-list/list/grid/actions.component.html @@ -87,11 +87,7 @@ label="FEMP CTS Export" > - + Salesforce diff --git a/src/app/modules/inventory/actions/ali-change-modal.component.ts b/src/app/modules/inventory/actions/ali-change-modal.component.ts index 94689ea6..99949a40 100644 --- a/src/app/modules/inventory/actions/ali-change-modal.component.ts +++ b/src/app/modules/inventory/actions/ali-change-modal.component.ts @@ -3,11 +3,11 @@ import type { OnDestroy, OnInit } from '@angular/core' import { Component, inject } from '@angular/core' import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms' import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' +import { combineLatest, Subject, take, takeUntil, tap } from 'rxjs' import type { AccessLevelInstancesByDepth, AccessLevelsByDepth } from '@seed/api' import { InventoryService, OrganizationService } from '@seed/api' import { ModalHeaderComponent } from '@seed/components' import { MaterialImports } from '@seed/materials' -import { combineLatest, Subject, take, takeUntil, tap } from 'rxjs' @Component({ selector: 'seed-ali-change-modal', diff --git a/src/app/modules/inventory/actions/analysis-config/bur-config.component.ts b/src/app/modules/inventory/actions/analysis-config/bur-config.component.ts index d39bbe8f..54a29091 100644 --- a/src/app/modules/inventory/actions/analysis-config/bur-config.component.ts +++ b/src/app/modules/inventory/actions/analysis-config/bur-config.component.ts @@ -2,11 +2,11 @@ import { CommonModule } from '@angular/common' import type { OnDestroy, OnInit } from '@angular/core' import { Component, EventEmitter, inject, Input, Output } from '@angular/core' import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms' -import { naturalSort } from '@seed/utils' import { Subject, takeUntil, tap } from 'rxjs' import type { Column } from '@seed/api' import { ColumnService } from '@seed/api' import { MaterialImports } from '@seed/materials' +import { naturalSort } from '@seed/utils' @Component({ selector: 'seed-bur-config', diff --git a/src/app/modules/inventory/actions/export-modal.component.html b/src/app/modules/inventory/actions/export-modal.component.html index 344bba3a..37f17567 100644 --- a/src/app/modules/inventory/actions/export-modal.component.html +++ b/src/app/modules/inventory/actions/export-modal.component.html @@ -1,15 +1,10 @@ - - +
-
+ Name @@ -22,21 +17,20 @@ @if (form.value.export_type === 'geojson') { - Include Meter Readings (Only - recommended for small exports) - } + Include Meter Readings (Only recommended for small exports) + - @else if (form.value.export_type === 'csv') { - Include Label Header - } + } @else if (form.value.export_type === 'csv') { + Include Label Header - @else { -
+ } @else { +
}
- +
@@ -48,7 +42,5 @@ [total]="progressBarObj.total" > -
- diff --git a/src/app/modules/inventory/actions/groups-modal.component.ts b/src/app/modules/inventory/actions/groups-modal.component.ts index dbcc7c91..e669449c 100644 --- a/src/app/modules/inventory/actions/groups-modal.component.ts +++ b/src/app/modules/inventory/actions/groups-modal.component.ts @@ -3,16 +3,16 @@ import type { OnDestroy, OnInit } from '@angular/core' import { Component, inject } from '@angular/core' import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms' import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' +import { AgGridAngular } from 'ag-grid-angular' +import type { CellValueChangedEvent, ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' +import { Subject, switchMap, take, takeUntil, tap } from 'rxjs' import type { CurrentUser, InventoryGroup } from '@seed/api' import { GroupsService, OrganizationService, UserService } from '@seed/api' import { AlertComponent, ModalHeaderComponent } from '@seed/components' import { MaterialImports } from '@seed/materials' import { ConfigService } from '@seed/services' import { SEEDValidators } from '@seed/validators' -import { AgGridAngular } from 'ag-grid-angular' -import type { CellValueChangedEvent, ColDef, GridApi, GridReadyEvent } from 'ag-grid-community' import type { InventoryDisplayType, InventoryType } from 'app/modules/inventory/inventory.types' -import { Subject, switchMap, take, takeUntil, tap } from 'rxjs' @Component({ selector: 'seed-inventory-groups-modal', diff --git a/src/app/modules/inventory/actions/labels-modal.component.ts b/src/app/modules/inventory/actions/labels-modal.component.ts index d614f12f..7d0824ea 100644 --- a/src/app/modules/inventory/actions/labels-modal.component.ts +++ b/src/app/modules/inventory/actions/labels-modal.component.ts @@ -1,18 +1,18 @@ import { CommonModule } from '@angular/common' -import type { OnDestroy, OnInit} from '@angular/core' +import type { OnDestroy, OnInit } from '@angular/core' import { Component, inject } from '@angular/core' import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms' import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' -import type { Label, LabelColor} from '@seed/api' +import { AgGridAngular } from 'ag-grid-angular' +import type { CellValueChangedEvent, ColDef } from 'ag-grid-community' +import { Subject, switchMap, takeUntil, tap } from 'rxjs' +import type { Label, LabelColor } from '@seed/api' import { LabelService } from '@seed/api' import { ModalHeaderComponent } from '@seed/components' import { MaterialImports } from '@seed/materials' -import { AgGridAngular } from 'ag-grid-angular' -import { Subject, switchMap, takeUntil, tap } from 'rxjs' -import { InventoryType } from '../inventory.types' -import { CellValueChangedEvent, ColDef } from 'ag-grid-community' import { ConfigService } from '@seed/services' import { SEEDValidators } from '@seed/validators' +import type { InventoryType } from '../inventory.types' @Component({ selector: 'seed-labels-modal', diff --git a/src/styles/styles.scss b/src/styles/styles.scss index c4ff4d7e..d9660949 100644 --- a/src/styles/styles.scss +++ b/src/styles/styles.scss @@ -3,7 +3,7 @@ // colors for text alerts :root { - --primary-text-color: rgb(29, 78, 216); + --primary-text-color: rgb(29 78 216); --success-text-color: rgb(21 128 61); --info-text-color: theme('colors.cyan.600'); --warning-text-color: rgb(180 83 9); @@ -241,14 +241,14 @@ .border-button-toggle-group { @apply rounded-full w-fit gap-0 !important; - border: 1px solid rgb(170, 170, 170) !important; + border: 1px solid rgb(170 170 170) !important; .mat-button-toggle { @apply rounded-none m-0 !important; } .mat-button-toggle:not(:first-child) { - border-left: 1px solid rgb(170, 170, 170) !important; + border-left: 1px solid rgb(170 170 170) !important; } // Uncomment to change checked button to primary color From 1e1b1ecb7142c3bbf2f435f7a6ea87261ac962e8 Mon Sep 17 00:00:00 2001 From: Ross Perry Date: Tue, 29 Jul 2025 17:50:15 +0000 Subject: [PATCH 16/16] lint --- .../list/actions/refresh-metadata-modal.component.html | 1 - src/app/modules/inventory/actions/export-modal.component.html | 2 -- src/styles/styles.scss | 1 + 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.html b/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.html index c847261c..23444d78 100644 --- a/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.html +++ b/src/app/modules/inventory-list/list/actions/refresh-metadata-modal.component.html @@ -2,7 +2,6 @@ @if (!inProgress) {
This will set the selected inventory's 'Updated' timestamp to {{ currentTime }}.
- } @else { Include Meter Readings (Only recommended for small exports) - } @else if (form.value.export_type === 'csv') { Include Label Header - } @else {
} diff --git a/src/styles/styles.scss b/src/styles/styles.scss index d9660949..7d8f6a06 100644 --- a/src/styles/styles.scss +++ b/src/styles/styles.scss @@ -241,6 +241,7 @@ .border-button-toggle-group { @apply rounded-full w-fit gap-0 !important; + border: 1px solid rgb(170 170 170) !important; .mat-button-toggle {