diff --git a/src/@seed/api/cache/cache.service.ts b/src/@seed/api/cache/cache.service.ts new file mode 100644 index 00000000..02dee11b --- /dev/null +++ b/src/@seed/api/cache/cache.service.ts @@ -0,0 +1,21 @@ +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' + +@Injectable({ providedIn: 'root' }) +export class CacheService { + private _errorService = inject(ErrorService) + private _httpClient = inject(HttpClient) + + getCacheEntry(orgId: number, uniqueId: number): Observable { + const url = `/api/v3/cache_entries/${uniqueId}/?organization_id=${orgId}` + return this._httpClient.get(url).pipe( + catchError((error: HttpErrorResponse) => { + return this._errorService.handleError(error, 'Error fetching cache entry') + }), + ) + } +} diff --git a/src/@seed/api/cache/index.ts b/src/@seed/api/cache/index.ts new file mode 100644 index 00000000..3d3b2722 --- /dev/null +++ b/src/@seed/api/cache/index.ts @@ -0,0 +1 @@ +export * from './cache.service' diff --git a/src/@seed/api/index.ts b/src/@seed/api/index.ts index a7a17037..e6d01803 100644 --- a/src/@seed/api/index.ts +++ b/src/@seed/api/index.ts @@ -1,5 +1,6 @@ export * from './analysis' export * from './audit-template' +export * from './cache' export * from './column' export * from './column-mapping-profile' export * from './config' diff --git a/src/@seed/api/inventory/inventory.service.ts b/src/@seed/api/inventory/inventory.service.ts index 35733f00..633ad51c 100644 --- a/src/@seed/api/inventory/inventory.service.ts +++ b/src/@seed/api/inventory/inventory.service.ts @@ -352,19 +352,9 @@ export class InventoryService { ) } - startInventoryExport(orgId: number): Observable { + startInventoryExport(orgId: number, data: InventoryExportData): 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( + return this._httpClient.post(url, data).pipe( take(1), catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error starting export') diff --git a/src/@seed/api/mapping/mapping.service.ts b/src/@seed/api/mapping/mapping.service.ts index 60cc1040..2d61c79a 100644 --- a/src/@seed/api/mapping/mapping.service.ts +++ b/src/@seed/api/mapping/mapping.service.ts @@ -3,7 +3,7 @@ import { HttpClient } from '@angular/common/http' import { inject, Injectable } from '@angular/core' import { catchError, map, type Observable } from 'rxjs' import { ErrorService } from '@seed/services' -import type { MappedData, MappingResultsResponse } from '../dataset' +import type { MappedData } from '../dataset' import type { ProgressResponse, SubProgressResponse } from '../progress' import { UserService } from '../user' import type { FirstFiveRowsResponse, MappingSuggestionsResponse, MatchingResultsResponse, RawColumnNamesResponse } from './mapping.types' @@ -66,9 +66,9 @@ export class MappingService { ) } - mappingResults(orgId: number, importFileId: number): Observable { + mappingResults(orgId: number, importFileId: number): Observable { const url = `/api/v3/import_files/${importFileId}/mapping_results/?organization_id=${orgId}` - return this._httpClient.post(url, {}) + return this._httpClient.post(url, {}) .pipe( catchError((error: HttpErrorResponse) => { return this._errorService.handleError(error, 'Error fetching mapping results') diff --git a/src/@seed/services/uploader/uploader.service.ts b/src/@seed/services/uploader/uploader.service.ts index 49f3dd21..8de40f64 100644 --- a/src/@seed/services/uploader/uploader.service.ts +++ b/src/@seed/services/uploader/uploader.service.ts @@ -7,6 +7,7 @@ import type { ProgressResponse } from '@seed/api' import { ErrorService } from '../error' import type { CheckProgressLoopParams, + ExportDataType, GreenButtonMeterPreview, MeterPreviewResponse, ProgressBarObj, @@ -51,7 +52,7 @@ export class UploaderService { switchMap(() => this.checkProgress(progressKey)), tap((response) => { this._updateProgressBarObj({ data: response, offset, multiplier, progressBarObj }) - if (response.status === 'success') successFn() + if (response.status === 'success') successFn(response) }), catchError(() => { // TODO the interval needs to continue if the error was network-related @@ -176,6 +177,26 @@ export class UploaderService { ) } + stringToBlob(data: string, exportType: ExportDataType) { + const base64ToBlob = (base64: string): Blob => { + const binary = atob(base64) + const bytes = new Uint8Array(binary.length) + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i) + } + + return new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) + } + + const blobMap: Record Blob> = { + csv: () => new Blob([data], { type: 'text/csv' }), + xlsx: () => base64ToBlob(data), + geojson: () => new Blob([JSON.stringify(data, null, '\t')], { type: 'application/geo+json' }), + } + + return blobMap[exportType]() + } + /* * Updates the progress bar object with incoming progress data. */ diff --git a/src/@seed/services/uploader/uploader.types.ts b/src/@seed/services/uploader/uploader.types.ts index ab406b8c..d7863e68 100644 --- a/src/@seed/services/uploader/uploader.types.ts +++ b/src/@seed/services/uploader/uploader.types.ts @@ -16,7 +16,7 @@ export type CheckProgressLoopParams = { progressKey: string; offset?: number; multiplier?: number; - successFn?: () => void; + successFn?: (response: ProgressResponse) => void; failureFn?: () => void; progressBarObj: ProgressBarObj; subProgress?: boolean; @@ -68,3 +68,5 @@ export type MeterPreviewResponse = { unlinkable_pm_ids: number[]; validated_type_units: ValidatedTypeUnit[]; } + +export type ExportDataType = 'csv' | 'xlsx' | 'geojson' diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.html b/src/app/modules/datasets/data-mappings/data-mapping.component.html index f5ac8942..099c942f 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.html +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.html @@ -56,7 +56,7 @@ - + diff --git a/src/app/modules/datasets/data-mappings/data-mapping.component.ts b/src/app/modules/datasets/data-mappings/data-mapping.component.ts index c128771b..4b4ddf24 100644 --- a/src/app/modules/datasets/data-mappings/data-mapping.component.ts +++ b/src/app/modules/datasets/data-mappings/data-mapping.component.ts @@ -7,7 +7,7 @@ import { ActivatedRoute } from '@angular/router' import { AgGridAngular } from 'ag-grid-angular' import { catchError, filter, forkJoin, of, Subject, switchMap, take, takeUntil, tap } from 'rxjs' import type { Column, ColumnMappingProfile, ColumnMappingProfileType, Cycle, ImportFile, MappingResultsResponse, MappingSuggestionsResponse, Organization, ProgressResponse } from '@seed/api' -import { ColumnMappingProfileService, ColumnService, CycleService, DatasetService, MappingService, OrganizationService, UserService } from '@seed/api' +import { CacheService, ColumnMappingProfileService, ColumnService, CycleService, DatasetService, MappingService, OrganizationService, UserService } from '@seed/api' import { PageComponent, ProgressBarComponent } from '@seed/components' import { MaterialImports } from '@seed/materials' import { UploaderService } from '@seed/services/uploader' @@ -40,6 +40,7 @@ export class DataMappingComponent implements OnDestroy, OnInit { @ViewChild(MapDataComponent) mapDataComponent!: MapDataComponent @ViewChild(MatchMergeComponent) matchMergeComponent!: MatchMergeComponent private readonly _unsubscribeAll$ = new Subject() + private _cacheService = inject(CacheService) private _columnMappingProfileService = inject(ColumnMappingProfileService) private _columnService = inject(ColumnService) private _cycleService = inject(CycleService) @@ -71,12 +72,12 @@ export class DataMappingComponent implements OnDestroy, OnInit { matchingTaxLotColumns: string[] = [] org: Organization orgId: number + progressBarObj = this._uploaderService.defaultProgressBarObj + progressTitle = 'Mapping Data...' propertyColumns: Column[] rawColumnNames: string[] = [] taxlotColumns: Column[] - progressBarObj = this._uploaderService.defaultProgressBarObj - ngOnInit(): void { // this._userService.currentOrganizationId$ this._organizationService.currentOrganization$ @@ -181,7 +182,6 @@ export class DataMappingComponent implements OnDestroy, OnInit { this._snackBar.alert('Error starting mapping') } const successFn = () => { - this.nextStep(2) this.getMappingResults() } @@ -214,10 +214,26 @@ export class DataMappingComponent implements OnDestroy, OnInit { } getMappingResults(): void { - this.nextStep(2) + this.progressTitle = 'Fetching Mapping Results...' + const successFn = ({ unique_id }: ProgressResponse) => { + this._cacheService.getCacheEntry(this.orgId, unique_id) + .pipe( + tap((response) => { + this.mappingResultsResponse = response as MappingResultsResponse + this.nextStep(2) + }), + take(1), + ) + .subscribe() + } + this._mappingService.mappingResults(this.orgId, this.fileId) .pipe( - tap((mappingResultsResponse) => { this.mappingResultsResponse = mappingResultsResponse }), + switchMap(({ progress_key }) => this._uploaderService.checkProgressLoop({ + progressKey: progress_key, + successFn, + progressBarObj: this.progressBarObj, + })), ) .subscribe() } diff --git a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html index 06f8c2a7..3f98c6f0 100644 --- a/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html +++ b/src/app/modules/datasets/data-mappings/step3/save-mappings.component.html @@ -72,13 +72,3 @@ > } - -@if (loading) { -
-
- -
Fetching Mapping Results...
-
- -
-} diff --git a/src/app/modules/inventory-detail/ubids/modal/form-modal.component.ts b/src/app/modules/inventory-detail/ubids/modal/form-modal.component.ts index 9461b6ff..48068f3d 100644 --- a/src/app/modules/inventory-detail/ubids/modal/form-modal.component.ts +++ b/src/app/modules/inventory-detail/ubids/modal/form-modal.component.ts @@ -62,10 +62,7 @@ export class FormModalComponent implements OnInit { this._ubidService .update(this.data.orgId, this.data.viewId, this.data.ubid.id, ubidDetails, this.data.type) .pipe( - tap((response) => { - console.log('response', response) - this.close(preferred) - }), + tap(() => { this.close(preferred) }), ) .subscribe() } else { @@ -74,10 +71,7 @@ export class FormModalComponent implements OnInit { this._ubidService .create(this.data.orgId, this.data.viewId, ubidDetails, this.data.type) .pipe( - tap((response) => { - console.log('response', response) - this.close(preferred) - }), + tap(() => { this.close(preferred) }), ) .subscribe() } diff --git a/src/app/modules/inventory/actions/export-modal.component.html b/src/app/modules/inventory/actions/export-modal.component.html index 7c9b568c..9bb85c2f 100644 --- a/src/app/modules/inventory/actions/export-modal.component.html +++ b/src/app/modules/inventory/actions/export-modal.component.html @@ -16,12 +16,11 @@ GeoJSON + Include Notes @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 index 86fbd81c..8e37cef9 100644 --- a/src/app/modules/inventory/actions/export-modal.component.ts +++ b/src/app/modules/inventory/actions/export-modal.component.ts @@ -3,8 +3,9 @@ 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 { catchError, EMPTY, Subject, switchMap, take, takeUntil, tap } from 'rxjs' +import type { ProgressResponse } from '@seed/api' +import { CacheService, InventoryService } from '@seed/api' import { ModalHeaderComponent, ProgressBarComponent } from '@seed/components' import { MaterialImports } from '@seed/materials' import { UploaderService } from '@seed/services' @@ -18,6 +19,7 @@ import type { InventoryExportData, InventoryType } from '../inventory.types' }) export class ExportModalComponent implements OnDestroy { @ViewChild('stepper') stepper!: MatStepper + private _cacheService = inject(CacheService) private _dialogRef = inject(MatDialogRef) private _inventoryService = inject(InventoryService) private _snackBar = inject(SnackBarService) @@ -36,41 +38,48 @@ export class ExportModalComponent implements OnDestroy { } 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_notes: new FormControl(false), include_meter_readings: new FormControl(false), + name: new FormControl(null, Validators.required), }) export() { - this._inventoryService.startInventoryExport(this.data.orgId) + const successFn = ({ unique_id }: ProgressResponse) => { + this._cacheService.getCacheEntry(this.data.orgId, unique_id).pipe( + tap((response: { data: string }) => { + const blob = this.getBlob(response.data) + this.downloadData(blob) + this.close() + }), + take(1), + ).subscribe() + } + + this.initExport() + this._inventoryService.startInventoryExport(this.data.orgId, this.exportData) .pipe( - tap(({ progress_key }) => { this.initExport(progress_key) }), - switchMap(() => this.pollExport()), - tap((response) => { this.downloadData(response[0]) }), + switchMap(({ progress_key }) => this._uploaderService.checkProgressLoop({ + progressKey: progress_key, + successFn, + failureFn: () => { this.close() }, + progressBarObj: this.progressBarObj, + })), takeUntil(this._unsubscribeAll$), catchError(() => { return EMPTY }), - finalize(() => { this.close() }), ) .subscribe() } - initExport(progress_key: string) { + initExport() { this.stepper.next() this.formatFilename() - this.formatExportData(this.filename, progress_key) + this.formatExportData(this.filename) } - 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, - }), - ]) + getBlob(data: string): Blob { + const exportType = this.form.value.export_type + return this._uploaderService.stringToBlob(data, exportType) } downloadData(data: Blob) { @@ -91,7 +100,7 @@ export class ExportModalComponent implements OnDestroy { } } - formatExportData(filename: string, progress_key: string) { + formatExportData(filename: string) { this.exportData = { export_type: this.form.value.export_type, filename, @@ -99,7 +108,6 @@ export class ExportModalComponent implements OnDestroy { include_meter_readings: this.form.value.include_meter_readings, include_notes: this.form.value.include_notes, profile_id: this.data.profileId, - progress_key, } } diff --git a/src/app/modules/inventory/inventory.types.ts b/src/app/modules/inventory/inventory.types.ts index 2f39f5ed..055dfeac 100644 --- a/src/app/modules/inventory/inventory.types.ts +++ b/src/app/modules/inventory/inventory.types.ts @@ -263,5 +263,4 @@ export type InventoryExportData = { include_meter_readings: boolean; include_notes: boolean; profile_id: number; - progress_key: string; }