diff --git a/cspell.json b/cspell.json index 1de76a65..a9ae3264 100644 --- a/cspell.json +++ b/cspell.json @@ -21,17 +21,21 @@ "useGitignore": true, "words": [ "BEDES", + "bsyncr", "CEJST", "eeej", "EPSG", + "EISA", "FEMP", "falsey", "greenbutton", "moveend", "NMEC", "overlaycontainer", + "pvwatts", "SRID", "Syncr", + "sqft", "ubids", "unlinkable", "unpair", diff --git a/src/@seed/api/analysis/analysis.service.ts b/src/@seed/api/analysis/analysis.service.ts index c2fcb6bc..771b4ef7 100644 --- a/src/@seed/api/analysis/analysis.service.ts +++ b/src/@seed/api/analysis/analysis.service.ts @@ -3,11 +3,12 @@ import { HttpClient } from '@angular/common/http' import { inject, Injectable } from '@angular/core' import type { Observable, Subscription } from 'rxjs' import { BehaviorSubject, catchError, interval, map, Subject, takeUntil, takeWhile, tap, withLatestFrom } from 'rxjs' +import type { FullProgressResponse } from '@seed/api' import { OrganizationService } from '@seed/api' import { ErrorService } from '@seed/services' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' import { UserService } from '../user' -import type { AnalysesMessage, Analysis, AnalysisResponse, AnalysisServiceType, AnalysisSummary, AnalysisView, AnalysisViews, ListAnalysesResponse, ListMessagesResponse, PropertyAnalysesResponse, View } from './analysis.types' +import type { AnalysesMessage, Analysis, AnalysisCreateData, AnalysisResponse, AnalysisServiceType, AnalysisSummary, AnalysisView, AnalysisViews, ListAnalysesResponse, ListMessagesResponse, PropertyAnalysesResponse, View } from './analysis.types' @Injectable({ providedIn: 'root' }) export class AnalysisService { @@ -159,6 +160,22 @@ export class AnalysisService { ) } + create(orgId: number, data: AnalysisCreateData): Observable { + const url = `/api/v3/analyses/?organization_id=${orgId}&start_analysis=true` + return this._httpClient.post(url, data).pipe( + tap((response) => { + if (response.status === 'error') { + return this._errorService.handleError(response.errors as HttpErrorResponse, 'Error creating analysis') + } + this._snackBar.success('Running Analysis') + this.getAnalyses(orgId) + }), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error creating analysis') + }), + ) + } + summary(orgId: number, cycleId: number): Observable { const url = `/api/v4/analyses/stats/?cycle_id=${cycleId}&organization_id=${orgId}` return this._httpClient.get(url).pipe( diff --git a/src/@seed/api/analysis/analysis.types.ts b/src/@seed/api/analysis/analysis.types.ts index 77d44980..780be4ed 100644 --- a/src/@seed/api/analysis/analysis.types.ts +++ b/src/@seed/api/analysis/analysis.types.ts @@ -25,6 +25,14 @@ export type Analysis = { _finished_with_tasks: boolean; // used to determine if an analysis has no currently running tasks } +export type AnalysisCreateData = { + name: string; + service: AnalysisServiceType; + configuration: Record; + property_view_ids: number[]; + access_level_instance_id: number; +} + export type AnalysisServiceType = 'BSyncr' | 'BETTER' | 'EUI' | 'CO2' | 'EEEJ' | 'Element Statistics' | 'Building Upgrade Recommendation' // Analysis by View type @@ -108,3 +116,23 @@ export type AnalysisSummaryStats = { display_name: string; is_extra_data: boolean; } + +export type SavingsTarget = 'AGGRESSIVE' | 'CONSERVATIVE' | 'NOMINAL' +export type SelectMetersType = 'all' | 'date_range' | 'select_cycle' +export type BenchmarkDataType = 'DEFAULT' | 'GENERATE' + +export type BETTERConfig = { + benchmark_data_type: BenchmarkDataType; + cycle_id: number; + enable_pvwatts: boolean; + meter: { start_date: string; end_date: string }; + min_model_r_squared: number; + preprocess_meters: boolean; + portfolio_analysis: boolean; + savings_target: SavingsTarget; + select_meters: SelectMetersType; +} + +export type AnalysisConfig = BETTERConfig | Record + +export type BSyncrModelTypes = 'Simple Linear Regression' | 'Three bsyncrOptionsParameter Linear Model Cooling' | 'Three Parameter Linear Model Heating' | 'Four Parameter Linear Model' diff --git a/src/@seed/api/data-quality/data-quality.service.ts b/src/@seed/api/data-quality/data-quality.service.ts index 57c58dd0..e35fb495 100644 --- a/src/@seed/api/data-quality/data-quality.service.ts +++ b/src/@seed/api/data-quality/data-quality.service.ts @@ -4,7 +4,7 @@ import { catchError, map, type Observable, ReplaySubject, switchMap, tap } from import { OrganizationService } from '@seed/api' import { ErrorService } from '@seed/services' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' -import type { DQCProgressResponse } from '../progress' +import type { FullProgressResponse } from '../progress' import type { DataQualityResults, DataQualityResultsResponse, Rule } from './data-quality.types' @Injectable({ providedIn: 'root' }) @@ -81,9 +81,9 @@ export class DataQualityService { ) } - startDataQualityCheckForImportFile(orgId: number, importFileId: number): Observable { + startDataQualityCheckForImportFile(orgId: number, importFileId: number): Observable { const url = `/api/v3/import_files/${importFileId}/start_data_quality_checks/?organization_id=${orgId}` - return this._httpClient.post(url, {}) + return this._httpClient.post(url, {}) .pipe( catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error starting data quality checks for import file') @@ -91,14 +91,14 @@ export class DataQualityService { ) } - startDataQualityCheckForOrg(orgId: number, property_view_ids: number[], taxlot_view_ids: number[], goal_id: number): Observable { + startDataQualityCheckForOrg(orgId: number, property_view_ids: number[], taxlot_view_ids: number[], goal_id: number): Observable { const url = `/api/v3/data_quality_checks/${orgId}/start/` const data = { property_view_ids, taxlot_view_ids, goal_id, } - return this._httpClient.post(url, data).pipe( + return this._httpClient.post(url, data).pipe( catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error fetching data quality results for organization') }), diff --git a/src/@seed/api/geocode/geocode.service.ts b/src/@seed/api/geocode/geocode.service.ts new file mode 100644 index 00000000..5e3fc35e --- /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 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' }) +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/groups/groups.service.ts b/src/@seed/api/groups/groups.service.ts index 1cdd80be..459896a9 100644 --- a/src/@seed/api/groups/groups.service.ts +++ b/src/@seed/api/groups/groups.service.ts @@ -4,6 +4,7 @@ import { inject, Injectable } from '@angular/core' import { BehaviorSubject, catchError, map, type Observable, take, tap } from 'rxjs' import { ErrorService } from '@seed/services' import { SnackBarService } from 'app/core/snack-bar/snack-bar.service' +import type { InventoryType } from 'app/modules/inventory' import { OrganizationService } from '../organization' import type { InventoryGroup, InventoryGroupResponse, InventoryGroupsResponse } from './groups.types' @@ -34,8 +35,9 @@ export class GroupsService { .subscribe() } - listForInventory(orgId: number, inventoryIds: number[]) { - const url = `/api/v3/inventory_groups/filter/?organization_id=${orgId}&inventory_type=properties` + // inventoryIds (Property/TaxLot[]) are not viewIds + listForInventory(orgId: number, inventoryIds: number[], type: InventoryType) { + const url = `/api/v3/inventory_groups/filter/?organization_id=${orgId}&inventory_type=${type}` const body = { selected: inventoryIds } this._httpClient .post(url, body) @@ -92,4 +94,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/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/@seed/api/inventory/inventory.service.ts b/src/@seed/api/inventory/inventory.service.ts index 0ebb4d69..35733f00 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, @@ -22,6 +23,7 @@ import type { UpdateInventoryResponse, ViewResponse, } from 'app/modules/inventory/inventory.types' +import type { ProgressResponse } from '../progress' import { UserService } from '../user' @Injectable({ providedIn: 'root' }) @@ -324,4 +326,75 @@ export class InventoryService { }), ) } + + movePropertiesToAccessLevelInstance(orgId: number, aliId: number, viewIds: number[]): Observable { + const url = `/api/v3/properties/move_properties_to/?organization_id=${orgId}` + const data = { property_view_ids: viewIds, access_level_instance_id: aliId } + return this._httpClient.post(url, data).pipe( + tap(() => { this._snackBar.success('Properties moved successfully') }), + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error moving properties') + }), + ) + } + + updateDerivedData(orgId: number, propertyViewIds: number[], taxlotViewIds: number[]): Observable { + const url = '/api/v3/tax_lot_properties/update_derived_data/' + const data = { + organization_id: orgId, + property_view_ids: propertyViewIds, + taxlot_view_ids: taxlotViewIds, + } + return this._httpClient.post(url, data).pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error updating derived data') + }), + ) + } + + 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') + }), + ) + } + + 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/@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