Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/@seed/api/geocode/geocode.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class GeocodeService {
private _errorService = inject(ErrorService)

geocode(orgId: number, viewIds: number[], type: InventoryType): Observable<unknown> {
const url = `/api/v3/geocode/geocode_by_ids/&organization_id=${orgId}`
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 : [],
Expand Down
4 changes: 2 additions & 2 deletions src/@seed/api/geocode/geocode.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type ConfidenceSummary = {
properties: InventoryConfidenceSummary;
taxlots: InventoryConfidenceSummary;
properties?: InventoryConfidenceSummary;
taxlots?: InventoryConfidenceSummary;
}

export type InventoryConfidenceSummary = {
Expand Down
1 change: 1 addition & 0 deletions src/@seed/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './groups'
export * from './inventory'
export * from './label'
export * from './mapping'
export * from './matching'
export * from './meters'
export * from './notes'
export * from './organization'
Expand Down
29 changes: 29 additions & 0 deletions src/@seed/api/inventory/inventory.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,4 +397,33 @@ export class InventoryService {
}),
)
}

propertiesMetersExist(orgId: number, propertyViewIds: number[]): Observable<boolean> {
const url = `/api/v3/properties/meters_exist/?organization_id=${orgId}`
return this._httpClient.post<boolean>(url, { property_view_ids: propertyViewIds }).pipe(
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error checking if meters exist for properties')
}),
)
}

evaluationExportToCts(orgId: number, viewIds: number[], filename: string): Observable<Blob> {
const url = `/api/v3/properties/evaluation_export_to_cts/?organization_id=${orgId}`
return this._httpClient.post(url, { filename, property_view_ids: viewIds }, { responseType: 'arraybuffer' }).pipe(
map((response) => new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error exporting to CTS')
}),
)
}

facilityBpsExportToCts(orgId: number, viewIds: number[], filename: string): Observable<Blob> {
const url = `/api/v3/properties/facility_bps_export_to_cts/?organization_id=${orgId}`
return this._httpClient.post(url, { filename, property_view_ids: viewIds }, { responseType: 'arraybuffer' }).pipe(
map((response) => new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error exporting to CTS')
}),
)
}
}
2 changes: 2 additions & 0 deletions src/@seed/api/matching/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './matching.service'
export * from './matching.types'
28 changes: 28 additions & 0 deletions src/@seed/api/matching/matching.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { HttpClient } from '@angular/common/http'
import { inject, Injectable } from '@angular/core'
import type { Observable } from 'rxjs'
import { catchError, 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 type { MergingResponse } from './matching.types'

@Injectable({ providedIn: 'root' })
export class MatchingService {
private _httpClient = inject(HttpClient)
private _errorService = inject(ErrorService)
private _snackBar = inject(SnackBarService)

mergeInventory(orgId: number, viewIds: number[], type: InventoryType): Observable<MergingResponse> {
const url = `/api/v3/${type}/merge/?organization_id=${orgId}`
const key = type === 'taxlots' ? 'taxlot_view_ids' : 'property_view_ids'
const data = { [key]: viewIds }
return this._httpClient.post<MergingResponse>(url, data)
.pipe(
tap(() => { this._snackBar.success('Successfully merged inventory') }),
catchError(() => {
return this._errorService.handleError(null, 'Error merging inventory')
}),
)
}
}
6 changes: 6 additions & 0 deletions src/@seed/api/matching/matching.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type MergingResponse = {
status: string;
match_link_count?: number;
match_merged_count?: number;
message?: string;
}
4 changes: 4 additions & 0 deletions src/@seed/api/organization/organization.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
OrganizationsResponse,
OrganizationUser,
OrganizationUserResponse,
OrganizationUserSettings,
OrganizationUsersResponse,
StartSavingAccessLevelInstancesRequest,
UpdateAccessLevelsRequest,
Expand All @@ -44,13 +45,15 @@ export class OrganizationService {

private _organizations = new ReplaySubject<BriefOrganization[]>(1)
private _currentOrganization = new ReplaySubject<Organization>(1)
private _orgUserSettings = new ReplaySubject<OrganizationUserSettings>(1)
private _organizationUsers = new ReplaySubject<OrganizationUser[]>(1)
private _accessLevelTree = new ReplaySubject<AccessLevelTree>(1)
private _accessLevelInstancesByDepth = new ReplaySubject<AccessLevelsByDepth>(1)
private _inventoryService = inject(InventoryService)

organizations$ = this._organizations.asObservable()
currentOrganization$ = this._currentOrganization.asObservable()
orgUserSettings$ = this._orgUserSettings.asObservable()
organizationUsers$ = this._organizationUsers.asObservable()
accessLevelTree$ = this._accessLevelTree.asObservable()
accessLevelInstancesByDepth$ = this._accessLevelInstancesByDepth.asObservable()
Expand Down Expand Up @@ -113,6 +116,7 @@ export class OrganizationService {
const data = { settings }
const url = `/api/v4/organization_users/${orgUserId}/?organization_id=${orgId}`
return this._httpClient.put<OrganizationUserResponse>(url, data).pipe(
tap(({ data }) => { this._orgUserSettings.next(data.settings) }),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error updating organization user')
}),
Expand Down
12 changes: 12 additions & 0 deletions src/@seed/api/organization/organization.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export type OrganizationUserSettings = {
profile?: UserSettingsProfiles;
crossCycles?: UserSettingsCrossCycles;
labels?: UserLabelSettings;
pins?: UserPinSettings;
}

type UserSettingsFilters = {
Expand All @@ -129,6 +130,17 @@ type UserSettingsCrossCycles = {
taxlots?: number[];
}

type UserPinSettings = {
properties?: {
left: string[];
right: string[];
};
taxlots?: {
left: string[];
right: string[];
};
}

type UserLabelSettings = { ids: number[]; operator: LabelOperator }

export type OrganizationUsersResponse = {
Expand Down
27 changes: 26 additions & 1 deletion src/@seed/api/postoffice/postoffice.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import { inject, Injectable } from '@angular/core'
import { catchError, map, type Observable, ReplaySubject, Subject, takeUntil, tap } from 'rxjs'
import { UserService } from '@seed/api'
import { ErrorService } from '@seed/services'
import type { CreateEmailTemplateResponse, EmailTemplate, ListEmailTemplatesResponse } from './postoffice.types'
import { SnackBarService } from 'app/core/snack-bar/snack-bar.service'
import type { CreateEmailTemplateResponse, EmailTemplate, ListEmailTemplatesResponse, SendEmailResponse } from './postoffice.types'

@Injectable({ providedIn: 'root' })
export class PostOfficeService {
private _httpClient = inject(HttpClient)
private _userService = inject(UserService)
private _errorService = inject(ErrorService)
private _emailTemplates = new ReplaySubject<EmailTemplate[]>()
private _snackBar = inject(SnackBarService)
private readonly _unsubscribeAll$ = new Subject<void>()
orgId: number
emailTemplates$ = this._emailTemplates.asObservable()
Expand Down Expand Up @@ -66,4 +68,27 @@ export class PostOfficeService {
}),
)
}

sendEmail(orgId: number, stateIds: number[], template_id: number, inventory_type: string): Observable<SendEmailResponse> {
const url = `/api/v3/postoffice_email/?organization_id=${orgId}`
const data = {
from_email: 'blankl@example.com', // Dummy email. The backend will assign the appropriate email.
template_id,
inventory_id: stateIds,
inventory_type,
}
return this._httpClient.post<SendEmailResponse>(url, data)
.pipe(
tap(({ status }) => {
if (status === 'success') {
this._snackBar.success('Successfully sent email')
} else {
this._snackBar.alert('Error sending email')
}
}),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error sending email')
}),
)
}
}
27 changes: 27 additions & 0 deletions src/@seed/api/postoffice/postoffice.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,30 @@ export type ListEmailTemplatesResponse = {
status: string;
data: EmailTemplate[];
}

export type SendEmailResponse = {
status: string;
data: SentEmailData;
}

export type SentEmailData = {
backend_alias: string;
bcc: string;
cc: string;
context: string;
created: string;
expires_at: string | null;
from_email: string;
headers: string;
html_message: string;
id: number;
last_updated: string;
message: string;
number_of_retries: number | null;
priority: number;
scheduled_time: string | null;
status: number;
subject: string;
template_id: number;
to: string;
}
52 changes: 51 additions & 1 deletion src/@seed/api/ubid/ubid.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ErrorService } from '@seed/services'
import { SnackBarService } from 'app/core/snack-bar/snack-bar.service'
import type { InventoryType, InventoryTypeSingular } from 'app/modules/inventory/inventory.types'
import { UserService } from '../user'
import type { Ubid, UbidDetails, UbidResponse, ValidateUbidResponse } from './ubid.types'
import type { DecodeResults, Ubid, UbidDetails, UbidResponse, ValidateUbidResponse } from './ubid.types'

@Injectable({ providedIn: 'root' })
export class UbidService {
Expand Down Expand Up @@ -91,4 +91,54 @@ export class UbidService {
}),
)
}

decodeResults(orgId: number, viewIds: number[], type: InventoryType): Observable<DecodeResults> {
const url = `/api/v3/ubid/decode_results/?organization_id=${orgId}`
const data = {
property_view_ids: type === 'properties' ? viewIds : [],
taxlot_view_ids: type === 'taxlots' ? viewIds : [],
}
return this._httpClient.post<DecodeResults>(url, data).pipe(
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error fetching UBID decode results')
}),
)
}

decodeByIds(orgId: number, viewIds: number[], type: InventoryType): Observable<{ status: string }> {
const url = `/api/v3/ubid/decode_by_ids/?organization_id=${orgId}`
const data = {
property_view_ids: type === 'properties' ? viewIds : [],
taxlot_view_ids: type === 'taxlots' ? viewIds : [],
}
return this._httpClient.post<{ status: string }>(url, data).pipe(
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error decoding UBIDs by ID')
}),
)
}

compareUbids(orgId: number, ubid1: string, ubid2: string): Observable<number> {
const url = `/api/v3/ubid/get_jaccard_index/?organization_id=${orgId}`
return this._httpClient.post<{ status: string; data: number }>(url, { ubid1, ubid2 }).pipe(
map(({ data }) => data),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error comparing UBIDs')
}),
)
}

getUbidModelsByView(orgId: number, viewId: number, type: InventoryType): Observable<Ubid[]> {
const url = `/api/v3/ubid/ubids_by_view/?organization_id=${orgId}`
const data = {
view_id: viewId,
type: type === 'taxlots' ? 'taxlot' : 'property',
}
return this._httpClient.post<{ status: string; data: Ubid[] }>(url, data).pipe(
map(({ data }) => data),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error fetching UBID models')
}),
)
}
}
6 changes: 6 additions & 0 deletions src/@seed/api/ubid/ubid.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ export type ValidateUbidResponse = {
ubid: string;
};
}

export type DecodeResults = {
ubid_not_decoded: number;
ubid_successfully_decoded: number;
ubid_unpopulated: number;
}
4 changes: 4 additions & 0 deletions src/@seed/api/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,5 +194,9 @@ export class UserService {
userSettings.sorts ??= {}
userSettings.sorts.properties ??= []
userSettings.sorts.taxlots ??= []

userSettings.pins ??= {}
userSettings.pins.properties ??= { left: [], right: [] }
userSettings.pins.taxlots ??= { left: [], right: [] }
}
}
7 changes: 5 additions & 2 deletions src/@seed/services/error/error.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ export class ErrorService {
getErrorData(error: HttpErrorResponse, defaultMessage: string) {
// Handle different error response structures
const err: unknown = error.error
const status = error.status ? `${error.status}: ` : ''

const isStr = typeof err === 'string'

// If the string is too long (likely html '<!DOCTYPE html>...'), return the default message
if (isStr && (err.length > 1000 || err.startsWith('<!DOCTYPE html>'))) return defaultMessage
if (isStr && (err.length > 1000 || err.startsWith('<!DOCTYPE html>'))) {
return `${status}${defaultMessage}`
}
if (isStr) return err

const isObj = typeof err === 'object' && err !== null
Expand All @@ -38,7 +41,7 @@ export class ErrorService {
return e.message ?? e.error ?? e.errors ?? null
}

return defaultMessage
return `${status}${defaultMessage}`
}

isObjOfArrayStrings(obj: unknown): obj is Record<string, string[]> {
Expand Down
2 changes: 2 additions & 0 deletions src/app/modules/datasets/dataset/dataset.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ export class DatasetComponent implements OnDestroy, OnInit {

downloadDocument(file: string, filename: string) {
const a = document.createElement('a')
// NOTE: downloads failing after a recent change. Requires further investigation
// const url = file.replace('/seed/', '/')
const url = file
a.href = url
a.download = filename
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,7 @@ export class BuildingFilesGridComponent implements OnInit {
setColumnDefs() {
this.columnDefs = [
{ field: 'file_type', headerName: 'File Type' },
{
field: 'filename',
headerName: 'File Name',
},
{ field: 'filename', headerName: 'File Name' },
{ field: 'created', headerName: 'Created' },
{ field: 'actions', headerName: 'Actions', cellRenderer: this.actionRenderer },
]
Expand Down Expand Up @@ -71,7 +68,6 @@ export class BuildingFilesGridComponent implements OnInit {
downloadDocument(data: unknown) {
const { file, filename } = data as { file: string; filename: string }

console.log('Developer Note: Downloads will fail until frontend and backend are on the same server')
const a = document.createElement('a')
const url = file
a.href = url
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class DocumentsGridComponent implements OnChanges, OnDestroy {
return `
<div class="flex gap-2 mt-2 align-center">
<span class="material-icons cursor-pointer text-secondary" title="Download" data-action="download">cloud_download</span>
<span class="material-icons cursor-pointer text-secondary" title="Edit" data-action="delete">clear</span>
<span class="material-icons cursor-pointer text-secondary" title="Delete" data-action="delete">clear</span>
</div>
`
}
Expand Down
Loading