Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Audit Template Settings #15

Merged
merged 11 commits into from
Feb 17, 2025
93 changes: 93 additions & 0 deletions src/@seed/api/audit-template/audit-template.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { HttpClient, type HttpErrorResponse } from '@angular/common/http'
import { inject, Injectable } from '@angular/core'
import { catchError, map, type Observable, ReplaySubject, Subject, takeUntil } from 'rxjs'
import { ErrorService } from '@seed/services/error/error.service'
import { UserService } from '../user'
import type {
AuditTemplateConfig,
AuditTemplateConfigCreateResponse,
AuditTemplateConfigResponse,
AuditTemplateReportType,
} from './audit-template.types'

@Injectable({ providedIn: 'root' })
export class AuditTemplateService {
private _httpClient = inject(HttpClient)
private _userService = inject(UserService)
private _errorService = inject(ErrorService)
private readonly _unsubscribeAll$ = new Subject<void>()
private _reportTypes = new ReplaySubject<AuditTemplateReportType[]>(1)
private _auditTemplateConfig = new ReplaySubject<AuditTemplateConfig>(1)
reportTypes$ = this._reportTypes.asObservable()
auditTemplateConfig$ = this._auditTemplateConfig.asObservable()

constructor() {
this._reportTypes.next([
{ name: 'ASHRAE Level 2 Report' }, // cspell:disable-line
{ name: 'Atlanta Report' },
{ name: 'Baltimore Energy Audit Report' },
{ name: 'Berkeley Report' },
{ name: 'BRICR Phase 0/1' },
{ name: 'Brisbane Energy Audit Report' },
{ name: 'DC BEPS Energy Audit Report' }, // cspell:disable-line
{ name: 'DC BEPS RCx Report' }, // cspell:disable-line
{ name: 'Demo City Report' },
{ name: 'Denver Energy Audit Report' },
{ name: 'EE-RLF Template' },
{ name: 'Energy Trust of Oregon Report' },
{ name: 'Los Angeles Report' },
{ name: 'Minneapolis Energy Evaluation Report' },
{ name: 'New York City Energy Efficiency Report' },
{ name: 'Office of Recapitalization Energy Audit Report' },
{ name: 'Open Efficiency Report' },
{ name: 'San Francisco Report' },
{ name: 'St. Louis RCx Report' },
{ name: 'St. Louis Report' },
{ name: 'WA Commerce Clean Buildings - Form D Report' },
{ name: 'WA Commerce Grants Report' },
])
this._userService.currentOrganizationId$.pipe(takeUntil(this._unsubscribeAll$)).subscribe((organizationId) => {
this.getConfigs(organizationId).subscribe()
})
}

getConfigs(org_id: number): Observable<AuditTemplateConfig> {
const url = `/api/v3/audit_template_configs/?organization_id=${org_id}`
return this._httpClient.get<AuditTemplateConfigResponse>(url).pipe(
map((response) => {
this._auditTemplateConfig.next(response.data[0])
return response.data[0]
}),
catchError((error: HttpErrorResponse) => {
// TODO need to figure out error handling
return this._errorService.handleError(error, 'Error fetching audit template configs')
}),
)
}

create(auditTemplateConfig: AuditTemplateConfig): Observable<AuditTemplateConfig> {
const url = `/api/v3/audit_template_configs/?organization_id=${auditTemplateConfig.organization}`
return this._httpClient.post<AuditTemplateConfigCreateResponse>(url, { ...auditTemplateConfig }).pipe(
map((r) => {
this._auditTemplateConfig.next(r.data)
return r.data
}),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error updating Audit Template Config')
}),
)
}

update(auditTemplateConfig: AuditTemplateConfig): Observable<AuditTemplateConfig | null> {
const url = `/api/v3/audit_template_configs/${auditTemplateConfig.id}/?organization_id=${auditTemplateConfig.organization}`
return this._httpClient.put<AuditTemplateConfigResponse>(url, { ...auditTemplateConfig }).pipe(
map((r) => {
this._auditTemplateConfig.next(r.data[0])
return r.data[0]
}),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error updating Audit Template Config')
}),
)
}
}
22 changes: 22 additions & 0 deletions src/@seed/api/audit-template/audit-template.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export type AuditTemplateReportType = {
name: string;
}

export type AuditTemplateConfig = {
id: string;
update_at_day: number;
update_at_hour: number;
update_at_minute: number;
last_update_date?: string;
organization: number;
}

export type AuditTemplateConfigResponse = {
status: string;
data: AuditTemplateConfig[];
}

export type AuditTemplateConfigCreateResponse = {
status: string;
data: AuditTemplateConfig;
}
2 changes: 2 additions & 0 deletions src/@seed/api/audit-template/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './audit-template.service'
export * from './audit-template.types'
23 changes: 16 additions & 7 deletions src/@seed/api/organization/organization.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,24 @@ export class OrganizationService {

getOrganizationUsers(orgId: number): void {
const url = `/api/v3/organizations/${orgId}/users/`
this._httpClient.get<OrganizationUsersResponse>(url)
this._httpClient
.get<OrganizationUsersResponse>(url)
.pipe(
map((response) => response.users.sort((a, b) => naturalSort(a.last_name, b.last_name))),
tap((users) => { this._organizationUsers.next(users) }),
tap((users) => {
this._organizationUsers.next(users)
}),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error fetching organization users')
}),
).subscribe()
)
.subscribe()
}

getOrganizationAccessLevelTree(orgId: number): void {
const url = `/api/v3/organizations/${orgId}/access_levels/tree`
this._httpClient.get<AccessLevelTreeResponse>(url)
this._httpClient
.get<AccessLevelTreeResponse>(url)
.pipe(
map((response) => {
// update response to include more usable accessLevelInstancesByDepth
Expand Down Expand Up @@ -145,9 +150,13 @@ export class OrganizationService {
}

/*
* Transform access level tree into a more usable format
*/
private _calculateAccessLevelInstancesByDepth(tree: AccessLevelNode[], depth: number, result: AccessLevelsByDepth = {}): AccessLevelsByDepth {
* Transform access level tree into a more usable format
*/
private _calculateAccessLevelInstancesByDepth(
tree: AccessLevelNode[],
depth: number,
result: AccessLevelsByDepth = {},
): AccessLevelsByDepth {
if (!tree) return result
if (!result[depth]) result[depth] = []
for (const ali of tree) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,174 @@
<h3 class="text-2xl font-bold">Audit Template Settings</h3>
<div class="prose">
If your organization has configured a customized report form in Audit Template, fill out the settings below to enable importing Audit
Template submissions into your SEED organization.
</div>
<div class="prose">
@if (organization$ | async; as o) {
Organization API Key: {{ o.name }}
<seed-page [config]="{ title: 'Organization Settings: Audit Template' }" title="Organization Settings: Audit Template">
@if (organization) {
<div class="flex-auto p-6 sm:p-10" *transloco="let t">
<div class="prose">
If your organization has configured a customized report form in Audit Template, fill out the settings below to enable importing
Audit Template submissions into your SEED organization.
</div>
<div class="max-w-2xl">
<form
class="bg-card flex flex-col overflow-hidden rounded-2xl p-2 pb-4 shadow"
[formGroup]="auditTemplateForm"
(ngSubmit)="submit()"
>
<div class="flex flex-col">
<div class="text-lg font-medium">Audit Template Credentials</div>
<div class="text-secondary prose mb-6">
<p>An API Token, Username and Password are all required to connect to your Audit Template.</p>
<p>
Please refer to the
<a class="text-blue-400" href="https://staging.labworks.org/reports/api/" target="_new">Audit Template documentation</a> for
more information.
</p>
</div>
<mat-form-field class="mb-6">
<mat-label>Audit Template Organization Token</mat-label>
<input matInput [formControlName]="'at_organization_token'" />
<mat-hint> Note, do not prefix the token with "Token ", only include the token itself. </mat-hint>
</mat-form-field>
<mat-form-field class="mb-6">
<mat-label>Audit Template Email</mat-label>
<input matInput [formControlName]="'audit_template_user'" />
<mat-hint>
Use the email associated with your account from the
<a class="text-blue-400" href="https://staging.labworks.org/">Building Energy Score Site</a>.
</mat-hint>
</mat-form-field>
<mat-form-field>
<mat-label>Audit Template Password </mat-label>
<input matInput [formControlName]="'audit_template_password'" [type]="passwordHidden ? 'password' : 'text'" />
<button
mat-icon-button
matSuffix
type="button"
(click)="togglePassword()"
[attr.aria-label]="'Hide password'"
[attr.aria-pressed]="passwordHidden"
>
<mat-icon class="icon-size-5" [svgIcon]="passwordHidden ? 'fa-solid:eye-slash' : 'fa-solid:eye'"></mat-icon>
</button>
<mat-hint>
Use the password associated with your account from the
<a class="text-blue-400" href="https://staging.labworks.org/">Building Energy Score Site</a>.
</mat-hint>
</mat-form-field>
</div>
<mat-divider role="separator" class="mat-divider mat-divider-horizontal mb-6 mt-4" aria-orientation="horizontal"></mat-divider>
<div class="flex flex-col">
<div class="text-lg font-medium">{{ t('Audit Template City ID') }}</div>
<div class="text-secondary prose mb-6">
<p>
Specify your Audit Template City ID. This number is visible in the Audit Template URL when browsing to the 'CITIES' tab.
SEED will import submission data for the specified City only.
</p>
</div>
<mat-form-field class="mb-6">
<mat-label>{{ t('Audit Template City ID') }}</mat-label>
<input matInput [formControlName]="'audit_template_city_id'" type="number" />
</mat-form-field>
</div>
<mat-divider role="separator" class="mat-divider mat-divider-horizontal mb-6 mt-4" aria-orientation="horizontal"></mat-divider>
<div class="flex flex-col">
<div class="text-lg font-medium">{{ t('Audit Template Submission Status') }}</div>
<div class="text-secondary prose mb-6">
<p>SEED will import data for submissions with the following statuses in Audit Template.</p>
</div>
<div class="flex flex-row">
<mat-checkbox formControlName="status_complies">Complies</mat-checkbox>
<mat-checkbox formControlName="status_pending">Pending</mat-checkbox>
<mat-checkbox formControlName="status_received">Received</mat-checkbox>
<mat-checkbox formControlName="status_rejected">Rejected</mat-checkbox>
</div>
</div>
<mat-divider role="separator" class="mat-divider mat-divider-horizontal mb-6 mt-4" aria-orientation="horizontal"></mat-divider>
<div class="flex flex-col">
<div class="text-lg font-medium">{{ t('Conditional Import') }}</div>
<div class="text-secondary prose mb-6">
<p>
When this checkbox is checked, SEED will only import Audit Template submissions that have been submitted more recently than
the SEED records' most recent update. If unchecked, all Audit Template submissions will be imported regardless of the
submission date.
</p>
</div>
<div class="flex flex-row">
<mat-slide-toggle formControlName="audit_template_conditional_import">{{ t('Enable Conditional Import') }}</mat-slide-toggle>
</div>
<div class="my-6">
<button mat-flat-button color="accent" type="button" (click)="importSubmissions()" [attr.aria-label]="'Import Submissions'">
Import Submissions
</button>
</div>
</div>
<mat-divider role="separator" class="mat-divider mat-divider-horizontal mb-6 mt-4" aria-orientation="horizontal"></mat-divider>
<div class="flex flex-col">
<div class="text-lg font-medium">{{ t('Schedule Weekly Update') }}</div>

<div class="mt-6 flex flex-row">
<mat-slide-toggle formControlName="audit_template_sync_enabled" (click)="updateScheduleInputs()">{{
t('Enable Audit Template Auto Sync')
}}</mat-slide-toggle>
</div>
<div class="text-secondary prose my-6">
If you would like to automatically update your SEED organization with Audit Template submission data for the selected Audit
Template City ID, configure the fields below to schedule your weekly update.
</div>
<div class="flex flex-row gap-2">
<mat-form-field class="flex">
<mat-label>{{ t('Day') }}</mat-label>
<mat-select formControlName="audit_template_config_day">
@for (d of days; track d.index) {
<mat-option [value]="d.index">{{ d.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="flex">
<mat-label>{{ t('Hour') }} (24)</mat-label>
<mat-select formControlName="audit_template_config_hour">
@for (h of hours; track h) {
<mat-option [value]="h">{{ h }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="flex">
<mat-label>{{ t('Minute') }}</mat-label>
<mat-select formControlName="audit_template_config_minute">
@for (m of minutes; track m) {
<mat-option [value]="m">{{ m }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<mat-divider role="separator" class="mat-divider mat-divider-horizontal mb-6 mt-4" aria-orientation="horizontal"></mat-divider>
<div class="flex flex-col">
<div class="text-lg font-medium">{{ t('Advanced Settings') }}</div>
<div class="text-secondary prose mb-6">
<p>
{{
t(
'If you wish to generate stub Audit Template reports from SEED data, select which Audit Template Report Type SEED should generate.'
)
}}
</p>
</div>

<mat-form-field class="mb-6">
<mat-label>{{ t('Audit Template Report Type') }}</mat-label>
<mat-select formControlName="audit_template_report_type">
<mat-option value="">None</mat-option>
@for (atrt of auditTemplateReportTypes; track atrt.name) {
<mat-option [value]="atrt.name">{{ atrt.name }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div>
<button mat-flat-button [disabled]="auditTemplateForm.invalid || auditTemplateForm.pending" color="primary">
<span class="">{{ t('Save Changes') }}</span>
</button>
</div>
</form>
</div>
</div>
}
</div>
</seed-page>
Loading