Skip to content

Commit f841121

Browse files
kfleminCopilotCopilot
authored
Analysis Functionality Parity (#61)
* analysis parity * Update public/i18n/en_US.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update public/i18n/fr_CA.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update public/i18n/es.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/@seed/components/analyses/analyses-grid.component.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/app/modules/inventory-detail/analyses/analyses.component.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: replace setTimeout with ChangeDetectorRef.markForCheck in analyses.component.ts Agent-Logs-Url: https://github.com/SEED-platform/seed-angular/sessions/58ed9bda-03a0-42bd-893f-7380bc268cee Co-authored-by: kflemin <2205659+kflemin@users.noreply.github.com> * fix: use ReplaySubject(1) for _unsubscribeAll$ to guard late setTimeout subscriptions Agent-Logs-Url: https://github.com/SEED-platform/seed-angular/sessions/d0093f52-622c-4515-a220-3164a6cf5983 Co-authored-by: kflemin <2205659+kflemin@users.noreply.github.com> * lint --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent d7391cf commit f841121

15 files changed

Lines changed: 202 additions & 25 deletions

File tree

public/i18n/en_US.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
"Analysis Name": "Analysis Name",
127127
"Analysis Name (User Defined)": "Analysis Name (User Defined)",
128128
"Analysis State": "Analysis State",
129+
"Analysis View": "Analysis View",
129130
"Apply Profile": "Apply Profile",
130131
"Are you absolutely sure you want to delete the organization": "Are you absolutely sure you want to delete the organization",
131132
"Are you absolutely sure you want to delete the sub-organization": "Are you absolutely sure you want to delete the sub-organization",
@@ -251,6 +252,7 @@
251252
"Collapse Tabs": "Collapse Tabs",
252253
"Column": "Column",
253254
"Column Description": "Column Description",
255+
"Column Detail Profiles": "Column Detail Profiles",
254256
"Column List Profile": "Column List Profile",
255257
"Column List Profiles": "Column List Profiles",
256258
"Column Mapping Profile": "Column Mapping Profile",
@@ -344,6 +346,7 @@
344346
"Create new label": "Create new label",
345347
"Create new sub-organization": "Create new sub-organization",
346348
"Created": "Created",
349+
"Cross Cycles": "Cross Cycles",
347350
"Cross-Cycles": "Cross-Cycles",
348351
"Current Column Mapping Profile": "Current Column Mapping Profile",
349352
"Current Cycle": "Current Cycle",
@@ -436,6 +439,7 @@
436439
"Designates whether the user can log into this admin site.": "Designates whether the user can log into this admin site.",
437440
"Designates whether this user should be treated as active. Unselect this instead of deleting accounts.": "Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
438441
"Desired Name": "Desired Name",
442+
"Detail": "Detail",
439443
"Detail Column List Profile": "Detail Column List Profile",
440444
"Determine Time Period from Field:": "Determine Time Period from Field:",
441445
"Developer": "Developer",
@@ -1431,6 +1435,7 @@
14311435
"UBID already exists": "UBID already exists",
14321436
"UBID comparison result:": "UBID comparison result:",
14331437
"UBID thresholds are between 0.0 and 1.0": "UBID thresholds are between 0.0 and 1.0",
1438+
"UBIDs": "UBIDs",
14341439
"UNMERGE_PROPERTIES_MODAL": "Are you sure you want to unmerge these properties? The unmerge action will keep meter data on both properties. Review meter data on both properties once they are unmerged.",
14351440
"UPDATE_DERIVED_DATA_TEXT_1": "Press the 'Begin Update' button below to begin the process of recalculating the data stored in derived columns for the selected properties and tax lots. The derived columns will appear yellow to indicate that the update is in progress.",
14361441
"UPDATE_DERIVED_DATA_TEXT_2": "Note that while the columns are yellow, the data may not be up to date. Refresh the page to refresh the columns status.",

public/i18n/es.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
"Analysis Name": "Análisis Nombre",
127127
"Analysis Name (User Defined)": "Nombre del análisis (definido por el usuario)",
128128
"Analysis State": "Análisis Estado",
129+
"Analysis View": "Vista de análisis",
129130
"Apply Profile": "Aplicar perfil",
130131
"Are you absolutely sure you want to delete the organization": "¿Está seguro de que desea eliminar la organización?",
131132
"Are you absolutely sure you want to delete the sub-organization": "¿Está seguro de que desea eliminar la suborganización?",
@@ -251,6 +252,7 @@
251252
"Collapse Tabs": "Contraer pestañas",
252253
"Column": "Columna",
253254
"Column Description": "Descripción de la columna",
255+
"Column Detail Profiles": "Perfiles de detalle de columnas",
254256
"Column List Profile": "Perfil de Column List",
255257
"Column List Profiles": "Perfiles de listas de columnas",
256258
"Column Mapping Profile": "Perfil de asignación de columnas",
@@ -344,6 +346,7 @@
344346
"Create new label": "Crear nueva etiqueta",
345347
"Create new sub-organization": "Crear una nueva suborganización",
346348
"Created": "Creado",
349+
"Cross Cycles": "Ciclos cruzados",
347350
"Cross-Cycles": "Ciclos cruzados",
348351
"Current Column Mapping Profile": "Perfil actual de asignación de columnas",
349352
"Current Cycle": "Ciclo actual",
@@ -436,6 +439,7 @@
436439
"Designates whether the user can log into this admin site.": "Indica si el usuario puede iniciar sesión en este sitio de administración.",
437440
"Designates whether this user should be treated as active. Unselect this instead of deleting accounts.": "Designa si este usuario debe ser tratado como activo. Deseleccione esta opción en lugar de eliminar cuentas.",
438441
"Desired Name": "Nombre deseado",
442+
"Detail": "Detalle",
439443
"Detail Column List Profile": "Perfil detallado de la lista de columnas",
440444
"Determine Time Period from Field:": "Determinar el periodo de tiempo a partir del campo:",
441445
"Developer": "Desarrollador",
@@ -1431,6 +1435,7 @@
14311435
"UBID already exists": "El UBID ya existe",
14321436
"UBID comparison result:": "Resultado de la comparación de UBID:",
14331437
"UBID thresholds are between 0.0 and 1.0": "Los umbrales UBID se sitúan entre 0,0 y 1,0",
1438+
"UBIDs": "UBIDs",
14341439
"UNMERGE_PROPERTIES_MODAL": "¿Está seguro de que desea desvincular estas propiedades? La acción de desvincular conservará los datos del medidor en ambas propiedades. Revise los datos del medidor en ambas propiedades una vez que se hayan desvinculado.",
14351440
"UPDATE_DERIVED_DATA_TEXT_1": "Pulse el botón \"Iniciar actualización\" para iniciar el proceso de recálculo de los datos almacenados en las columnas derivadas para las propiedades y lotes fiscales seleccionados. Las columnas derivadas aparecerán en amarillo para indicar que la actualización está en curso.",
14361441
"UPDATE_DERIVED_DATA_TEXT_2": "Tenga en cuenta que mientras las columnas estén en amarillo, los datos pueden no estar actualizados. Actualice la página para actualizar el estado de las columnas.",

public/i18n/fr_CA.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
"Analysis Name": "Nom de l'analyse",
127127
"Analysis Name (User Defined)": "Nom de l'analyse (défini par l'utilisateur)",
128128
"Analysis State": "État d'analyse",
129+
"Analysis View": "Vue d'analyse",
129130
"Apply Profile": "Appliquer le profil",
130131
"Are you absolutely sure you want to delete the organization": "Êtes-vous absolument sûr de vouloir supprimer l'organisation",
131132
"Are you absolutely sure you want to delete the sub-organization": "Êtes-vous absolument sûr de vouloir supprimer la sous-organisation",
@@ -251,6 +252,7 @@
251252
"Collapse Tabs": "Réduire les onglets",
252253
"Column": "Colonne",
253254
"Column Description": "Description de la colonne",
255+
"Column Detail Profiles": "Profils détaillés des colonnes",
254256
"Column List Profile": "Profil de liste de colonnes",
255257
"Column List Profiles": "Paramètres de la liste",
256258
"Column Mapping Profile": "Profil de mappage de colonne",
@@ -344,6 +346,7 @@
344346
"Create new label": "Créer une nouvelle étiquette",
345347
"Create new sub-organization": "Créer une nouvelle sous-organisation",
346348
"Created": "Créé",
349+
"Cross Cycles": "Cross Cycles",
347350
"Cross-Cycles": "Entre les Cycles",
348351
"Current Column Mapping Profile": "Profil de mappage de colonne actuel",
349352
"Current Cycle": "Cycle actuel",
@@ -436,6 +439,7 @@
436439
"Designates whether the user can log into this admin site.": "Désigne si l'utilisateur peut se connecter à ce site d'administration.",
437440
"Designates whether this user should be treated as active. Unselect this instead of deleting accounts.": "Indique si cet utilisateur doit être traité comme actif. Désélectionnez ceci au lieu de supprimer des comptes.",
438441
"Desired Name": "Nom Désiré",
442+
"Detail": "Détail",
439443
"Detail Column List Profile": "Profil de liste de colonnes de détail",
440444
"Determine Time Period from Field:": "Déterminer la durée du champ:",
441445
"Developer": "Développeur",
@@ -1431,6 +1435,7 @@
14311435
"UBID already exists": "UBID existe déjà",
14321436
"UBID comparison result:": "Résultat de la comparaison UBID :",
14331437
"UBID thresholds are between 0.0 and 1.0": "Les seuils UBID sont compris entre 0,0 et 1,0",
1438+
"UBIDs": "UBID",
14341439
"UNMERGE_PROPERTIES_MODAL": "Êtes-vous sûr de vouloir dissocier ces propriétés? L'action de dissociation conservera les données de compteur sur les deux propriétés. Vérifiez les données de compteur sur les deux propriétés une fois qu'elles sont dissociées.",
14351440
"UPDATE_DERIVED_DATA_TEXT_1": "Appuyez sur le bouton « Démarrer la mise à jour » ci-dessous pour commencer le processus de recalcul des données stockées dans les colonnes dérivées pour les propriétés et les lots fiscaux sélectionnés. Les colonnes dérivées apparaîtront en jaune pour indiquer que la mise à jour est en cours.",
14361441
"UPDATE_DERIVED_DATA_TEXT_2": "Notez que pendant que les colonnes sont jaunes, les données peuvent ne pas être à jour. Actualisez la page pour actualiser l'état des colonnes.",

src/@seed/api/analysis/analysis.service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ export class AnalysisService {
259259
EUI: "The EUI analysis will sum the property's meter readings for the last twelve months to calculate the energy use per square footage per year. If there are missing meter readings, then the analysis will return a less that 100% coverage to alert the user that there is a missing meter reading.",
260260
'Element Statistics':
261261
"The Element Statistics analysis looks through a property's element data (if any) to count the number of elements of type 'D.D.C. Control Panel'. It also generates the aggregated (average) condition index values for scope 1 emission elements and saves those quantities to the property.",
262+
'HVAC Metrics':
263+
'The HVAC Metrics analysis calculates HVAC-related metrics for the property based on floor area and available meter data.',
262264
}
263265
return descriptionMap[analysis.service] ?? 'No description available for this analysis.'
264266
}

src/@seed/api/analysis/analysis.types.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,15 @@ export type AnalysisCreateData = {
3333
access_level_instance_id: number;
3434
}
3535

36-
export type AnalysisServiceType = 'BSyncr' | 'BETTER' | 'EUI' | 'CO2' | 'EEEJ' | 'Element Statistics' | 'Building Upgrade Recommendation'
36+
export type AnalysisServiceType
37+
= | 'BSyncr'
38+
| 'BETTER'
39+
| 'EUI'
40+
| 'CO2'
41+
| 'EEEJ'
42+
| 'Element Statistics'
43+
| 'Building Upgrade Recommendation'
44+
| 'HVAC Metrics'
3745

3846
// Analysis by View type
3947
export type View = {

src/@seed/components/analyses/analyses-grid.component.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { Router } from '@angular/router'
66
import { AgGridAngular } from 'ag-grid-angular'
77
import type { CellClickedEvent, ColDef, GridApi, GridReadyEvent } from 'ag-grid-community'
88
import { filter, switchMap, take } from 'rxjs'
9-
import type { Analysis, Cycle, Highlight } from '@seed/api'
10-
import { AnalysisService } from '@seed/api'
9+
import type { Analysis, CurrentUser, Cycle, Highlight } from '@seed/api'
10+
import { AnalysisService, UserService } from '@seed/api'
1111
import { DeleteModalComponent } from '@seed/components'
1212
import { MaterialImports } from '@seed/materials'
1313
import { ConfigService } from '@seed/services'
@@ -25,15 +25,22 @@ export class AnalysesGridComponent implements AfterViewInit, OnChanges {
2525
@Input() highlights = false
2626
private _analysisService = inject(AnalysisService)
2727
private _configService = inject(ConfigService)
28+
private _userService = inject(UserService)
2829
private _router = inject(Router)
2930
private _dialog = inject(MatDialog)
31+
currentUser: CurrentUser
3032

3133
gridApi: GridApi
3234
gridTheme$ = this._configService.gridTheme$
3335
gridHeight = 0
3436
columnDefs: ColDef[] = []
3537

3638
ngAfterViewInit(): void {
39+
this._userService.currentUser$.pipe(take(1)).subscribe((user) => {
40+
this.currentUser = user
41+
this.setColumnDefs()
42+
this.gridApi?.refreshCells({ force: true })
43+
})
3744
this._analysisService.pollStatuses(this.orgId)
3845
}
3946

@@ -93,14 +100,21 @@ export class AnalysesGridComponent implements AfterViewInit, OnChanges {
93100
}
94101

95102
statusRenderer = ({ value }: { value: string }) => {
96-
const styleMap = {
97-
Completed: 'bg-green-900 text-white',
98-
Failed: 'bg-red-900 text-white',
103+
const styleMap: Record<string, string> = {
104+
Completed: 'background-color: #198754; color: white;',
105+
Failed: 'background-color: #dc3545; color: white;',
106+
Running: '',
107+
Creating: '',
108+
}
109+
const classMap: Record<string, string> = {
99110
Running: 'bg-primary text-white animate-pulse',
100111
Creating: 'bg-primary text-white animate-pulse',
101112
}
102113

103-
return `<div class="overflow-hidden ${styleMap[value]} px-2">${value}</div>`
114+
const style = styleMap[value] ?? ''
115+
const classes = classMap[value] ?? ''
116+
117+
return `<div class="overflow-hidden ${classes} px-2" style="${style}">${value}</div>`
104118
}
105119

106120
resultsRenderer = ({ data }: { data: Analysis }) => {
@@ -134,6 +148,8 @@ export class AnalysesGridComponent implements AfterViewInit, OnChanges {
134148
}
135149

136150
actionRenderer = ({ data }: { data: Analysis }) => {
151+
if (this.currentUser?.org_role === 'viewer') return ''
152+
137153
const runningStatuses = new Set(['Pending Creation', 'Creating', 'Queued', 'Running'])
138154
const isRunning = runningStatuses.has(data.status)
139155

src/app/modules/analyses/analysis/analysis.component.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,13 @@ export class AnalysisComponent implements OnDestroy, OnInit {
134134
}
135135

136136
statusRenderer = ({ value }: { value: string }) => {
137-
const bgColor = value === 'Completed' ? 'bg-green-900 text-white' : value === 'Failed' ? 'bg-red-900 text-white' : ''
138-
return `<div class="overflow-hidden ${bgColor} px-2">${value}</div>`
137+
const style
138+
= value === 'Completed'
139+
? 'background-color: #198754; color: white;'
140+
: value === 'Failed'
141+
? 'background-color: #dc3545; color: white;'
142+
: ''
143+
return `<div class="overflow-hidden px-2" style="${style}">${value}</div>`
139144
}
140145

141146
getCycle(params: { value: number[] }): string {

src/app/modules/inventory-detail/analyses/analyses.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
titleIcon: 'fa-solid:chart-simple',
66
breadcrumbs: ['Property', 'Analyses'],
77
sideNavToggle: true,
8-
action: createAnalysis,
9-
actionIcon: 'fa-solid:plus',
10-
actionText: 'Create Analysis',
8+
action: isViewer ? undefined : createAnalysis,
9+
actionIcon: isViewer ? undefined : 'fa-solid:plus',
10+
actionText: isViewer ? undefined : 'Create Analysis',
1111
}"
1212
>
1313
<div class="h-full-page-tabs p-5" id="content" #parentDiv>

src/app/modules/inventory-detail/analyses/analyses.component.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,35 @@
11
import { CommonModule } from '@angular/common'
2-
import type { OnInit } from '@angular/core'
3-
import { Component, inject } from '@angular/core'
2+
import type { OnDestroy, OnInit } from '@angular/core'
3+
import { ChangeDetectorRef, Component, inject } from '@angular/core'
4+
import { MatDialog } from '@angular/material/dialog'
45
import { ActivatedRoute } from '@angular/router'
56
import type { Observable } from 'rxjs'
6-
import { Subject, switchMap, takeUntil, tap } from 'rxjs'
7-
import type { Analysis, Cycle } from '@seed/api'
7+
import { filter, ReplaySubject, switchMap, takeUntil, tap } from 'rxjs'
8+
import type { Analysis, CurrentUser, Cycle } from '@seed/api'
89
import { AnalysisService, CycleService, InventoryService, OrganizationService, UserService } from '@seed/api'
910
import { AnalysesGridComponent, NotFoundComponent, PageComponent } from '@seed/components'
1011
import { SharedImports } from '@seed/directives'
12+
import { AnalysisRunModalComponent } from 'app/modules/inventory/actions/analysis-run-modal.component'
1113
import type { InventoryType, ViewResponse } from 'app/modules/inventory/inventory.types'
1214

1315
@Component({
1416
selector: 'seed-inventory-detail-analyses',
1517
templateUrl: './analyses.component.html',
1618
imports: [AnalysesGridComponent, CommonModule, NotFoundComponent, PageComponent, SharedImports],
1719
})
18-
export class AnalysesComponent implements OnInit {
20+
export class AnalysesComponent implements OnInit, OnDestroy {
1921
private _analysisService = inject(AnalysisService)
2022
private _cycleService = inject(CycleService)
2123
private _userService = inject(UserService)
2224
private _organizationService = inject(OrganizationService)
2325
private _inventoryService = inject(InventoryService)
2426
private _route = inject(ActivatedRoute)
25-
private readonly _unsubscribeAll$ = new Subject<void>()
27+
private _dialog = inject(MatDialog)
28+
private _cdr = inject(ChangeDetectorRef)
29+
private readonly _unsubscribeAll$ = new ReplaySubject<void>(1)
2630
analyses: Analysis[] = []
2731
cycles: Cycle[] = []
32+
currentUser: CurrentUser
2833
orgId: number
2934
viewDisplayField$: Observable<string>
3035
viewId: number
@@ -39,6 +44,10 @@ export class AnalysesComponent implements OnInit {
3944
}
4045

4146
ngOnInit(): void {
47+
this._userService.currentUser$.pipe(takeUntil(this._unsubscribeAll$)).subscribe((user) => {
48+
this.currentUser = user
49+
})
50+
4251
this.getParams()
4352
.pipe(
4453
switchMap(() => this._userService.currentOrganizationId$),
@@ -50,8 +59,12 @@ export class AnalysesComponent implements OnInit {
5059
this.cycles = cycles
5160
}),
5261
tap(() => {
53-
this.watchAnalyses()
62+
// Defer to avoid ExpressionChangedAfterItHasBeenCheckedError in LoadingBarComponent
63+
setTimeout(() => {
64+
this.watchAnalyses()
65+
})
5466
}),
67+
takeUntil(this._unsubscribeAll$),
5568
)
5669
.subscribe()
5770
}
@@ -77,6 +90,7 @@ export class AnalysesComponent implements OnInit {
7790
}),
7891
tap((analyses) => {
7992
this.analyses = analyses
93+
this._cdr.markForCheck()
8094
}),
8195
)
8296
}
@@ -90,7 +104,27 @@ export class AnalysesComponent implements OnInit {
90104
.subscribe()
91105
}
92106

93-
createAnalysis() {
94-
console.log('Create Analysis')
107+
createAnalysis = () => {
108+
const dialogRef = this._dialog.open(AnalysisRunModalComponent, {
109+
width: '40rem',
110+
data: { orgId: this.orgId, viewIds: [this.viewId] },
111+
})
112+
113+
dialogRef
114+
.afterClosed()
115+
.pipe(
116+
filter(Boolean),
117+
switchMap(() => this.getAnalyses()),
118+
)
119+
.subscribe()
120+
}
121+
122+
get isViewer() {
123+
return this.currentUser?.org_role === 'viewer'
124+
}
125+
126+
ngOnDestroy(): void {
127+
this._unsubscribeAll$.next()
128+
this._unsubscribeAll$.complete()
95129
}
96130
}

src/app/modules/inventory-detail/detail/header.component.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { AccessLevelInstance, Label, Organization } from '@seed/api'
1010
import { LabelComponent } from '@seed/components'
1111
import { MaterialImports } from '@seed/materials'
1212
import { ConfigService } from '@seed/services'
13+
import { AnalysisRunModalComponent } from 'app/modules/inventory/actions/analysis-run-modal.component'
1314
import type { GenericView, GroupMapping, Profile, ViewResponse } from 'app/modules/inventory/inventory.types'
1415
import { ModalComponent } from '../../column-list-profile/modal/modal.component'
1516
import { MapComponent } from './map.component'
@@ -110,9 +111,9 @@ export class HeaderComponent implements OnInit {
110111
{
111112
name: 'Run Analysis',
112113
action: () => {
113-
this.tempAction()
114+
this.openRunAnalysisModal()
114115
},
115-
disabled: true,
116+
disabled: false,
116117
},
117118
{
118119
name: 'Update with Audit Template',
@@ -214,6 +215,27 @@ export class HeaderComponent implements OnInit {
214215
.subscribe()
215216
}
216217

218+
openRunAnalysisModal() {
219+
const dialogRef = this._dialog.open(AnalysisRunModalComponent, {
220+
width: '40rem',
221+
data: {
222+
orgId: this.org.id,
223+
viewIds: [this.selectedView.id],
224+
},
225+
})
226+
227+
dialogRef
228+
.afterClosed()
229+
.pipe(
230+
take(1),
231+
filter(Boolean),
232+
tap(() => {
233+
this.refreshDetail.emit()
234+
}),
235+
)
236+
.subscribe()
237+
}
238+
217239
trackByFn(_index: number, { id }: AccessLevelInstance) {
218240
return id
219241
}

0 commit comments

Comments
 (0)