Skip to content

Commit 3b50924

Browse files
kfleminCopilotCopilot
authored
Implement default reports (#62)
* implement default repports * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: store absolute access_level_depth in report config save/new, fix load conversion Agent-Logs-Url: https://github.com/SEED-platform/seed-angular/sessions/ea0c2cf7-5d75-4950-9ca3-129d5e35aadd Co-authored-by: kflemin <2205659+kflemin@users.noreply.github.com> * fix: derive scatter category labels from chart_data, reset loading on error, remove redundant CSRF handling Agent-Logs-Url: https://github.com/SEED-platform/seed-angular/sessions/e265961d-9aad-44c3-be72-f06517de29ff Co-authored-by: kflemin <2205659+kflemin@users.noreply.github.com> * lint --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent f841121 commit 3b50924

17 files changed

Lines changed: 1493 additions & 10 deletions

.spelling.dic

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,4 @@ unpairing
7777
unrs
7878
unsubscription
7979
xmark
80+
csrftoken

proxy.conf.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ export default {
88
changeOrigin: true,
99
logLevel: 'debug',
1010
secure: false,
11+
onProxyReq: (proxyReq) => {
12+
const target = process.env.SEED_HOST ?? 'http://127.0.0.1:8000'
13+
proxyReq.setHeader('origin', target)
14+
proxyReq.setHeader('referer', `${target}/`)
15+
},
1116
},
1217
'/media/': {
1318
target: process.env.SEED_HOST ?? 'http://127.0.0.1:8000',

src/@seed/api/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './geocode'
1212
export * from './filter-group'
1313
export * from './groups'
1414
export * from './inventory'
15+
export * from './inventory-report'
1516
export * from './label'
1617
export * from './mapping'
1718
export * from './matching'
@@ -21,6 +22,7 @@ export * from './organization'
2122
export * from './pairing'
2223
export * from './postoffice'
2324
export * from './program'
25+
export * from './report-configuration'
2426
export * from './progress'
2527
export * from './salesforce'
2628
export * from './scenario'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './inventory-report.service'
2+
export * from './inventory-report.types'
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { HttpErrorResponse } from '@angular/common/http'
2+
import { HttpClient, HttpParams } from '@angular/common/http'
3+
import { inject, Injectable } from '@angular/core'
4+
import type { Observable } from 'rxjs'
5+
import { catchError, map } from 'rxjs'
6+
import { ErrorService } from '@seed/services'
7+
import type { AggregatedReportDataResponse, ReportDataResponse } from './inventory-report.types'
8+
9+
@Injectable({ providedIn: 'root' })
10+
export class InventoryReportService {
11+
private _errorService = inject(ErrorService)
12+
private _httpClient = inject(HttpClient)
13+
14+
getReportData(
15+
orgId: number,
16+
xVar: string,
17+
yVar: string,
18+
cycleIds: number[],
19+
accessLevelInstanceId: number | null,
20+
filterGroupId: number | null,
21+
): Observable<ReportDataResponse['data']> {
22+
const url = `/api/v3/organizations/${orgId}/report/`
23+
let params = new HttpParams().set('x_var', xVar).set('y_var', yVar)
24+
for (const id of cycleIds) {
25+
params = params.append('cycle_ids', id)
26+
}
27+
if (accessLevelInstanceId != null) params = params.set('access_level_instance_id', accessLevelInstanceId)
28+
if (filterGroupId != null) params = params.set('filter_group_id', filterGroupId)
29+
30+
return this._httpClient.get<ReportDataResponse>(url, { params }).pipe(
31+
map(({ data }) => data),
32+
catchError((error: HttpErrorResponse) => {
33+
return this._errorService.handleError(error, 'Error fetching report data')
34+
}),
35+
)
36+
}
37+
38+
getAggregatedReportData(
39+
orgId: number,
40+
xVar: string,
41+
yVar: string,
42+
cycleIds: number[],
43+
accessLevelInstanceId: number | null,
44+
filterGroupId: number | null,
45+
aggregationType: string,
46+
): Observable<AggregatedReportDataResponse['aggregated_data']> {
47+
const url = `/api/v3/organizations/${orgId}/report_aggregated/`
48+
let params = new HttpParams().set('x_var', xVar).set('y_var', yVar).set('aggregationType', aggregationType)
49+
for (const id of cycleIds) {
50+
params = params.append('cycle_ids', id)
51+
}
52+
if (accessLevelInstanceId != null) params = params.set('access_level_instance_id', accessLevelInstanceId)
53+
if (filterGroupId != null) params = params.set('filter_group_id', filterGroupId)
54+
55+
return this._httpClient.get<AggregatedReportDataResponse>(url, { params }).pipe(
56+
map(({ aggregated_data }) => aggregated_data),
57+
catchError((error: HttpErrorResponse) => {
58+
return this._errorService.handleError(error, 'Error fetching aggregated report data')
59+
}),
60+
)
61+
}
62+
63+
exportReportData(
64+
orgId: number,
65+
xVar: string,
66+
xLabel: string,
67+
yVar: string,
68+
yLabel: string,
69+
cycleIds: number[],
70+
filterGroupId: number | null,
71+
): Observable<Blob> {
72+
const url = `/api/v3/organizations/${orgId}/report_export/`
73+
let params = new HttpParams().set('x_var', xVar).set('x_label', xLabel).set('y_var', yVar).set('y_label', yLabel)
74+
for (const id of cycleIds) {
75+
params = params.append('cycle_ids', id)
76+
}
77+
if (filterGroupId != null) params = params.set('filter_group_id', filterGroupId)
78+
79+
return this._httpClient.get(url, { params, responseType: 'arraybuffer' }).pipe(
80+
map((buffer) => new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })),
81+
catchError((error: HttpErrorResponse) => {
82+
return this._errorService.handleError(error, 'Error exporting report data')
83+
}),
84+
)
85+
}
86+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
export type ReportChartPoint = {
2+
id: number;
3+
yr_e: string;
4+
x: number | string;
5+
y: number | string;
6+
display_name?: string;
7+
}
8+
9+
export type ReportPropertyCount = {
10+
yr_e: string;
11+
cycle: string;
12+
num_properties: number;
13+
'num_properties_w-data': number;
14+
color?: string;
15+
}
16+
17+
export type ReportAxisStatValues = {
18+
values: number[];
19+
children?: Record<string, number[]>;
20+
}
21+
22+
export type ReportAxisData = Record<string, Record<string, ReportAxisStatValues>>
23+
24+
export type ReportDataResponse = {
25+
status: string;
26+
data: {
27+
chart_data: ReportChartPoint[];
28+
property_counts: ReportPropertyCount[];
29+
axis_data: ReportAxisData;
30+
};
31+
}
32+
33+
export type AggregatedChartPoint = {
34+
x: number | string;
35+
y: number | string;
36+
yr_e: string;
37+
}
38+
39+
export type AggregatedReportDataResponse = {
40+
status: string;
41+
aggregated_data: {
42+
chart_data: AggregatedChartPoint[];
43+
property_counts: ReportPropertyCount[];
44+
};
45+
}
46+
47+
export type AxisVariable = {
48+
name: string;
49+
label: string;
50+
varName: string;
51+
axisLabel: string;
52+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './report-configuration.service'
2+
export * from './report-configuration.types'
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { HttpErrorResponse } from '@angular/common/http'
2+
import { HttpClient } from '@angular/common/http'
3+
import { inject, Injectable } from '@angular/core'
4+
import type { Observable } from 'rxjs'
5+
import { BehaviorSubject, catchError, map, tap } from 'rxjs'
6+
import { ErrorService } from '@seed/services'
7+
import { naturalSort } from '@seed/utils'
8+
import { SnackBarService } from 'app/core/snack-bar/snack-bar.service'
9+
import { UserService } from '../user'
10+
import type {
11+
ReportConfiguration,
12+
ReportConfigurationResponse,
13+
ReportConfigurationsResponse,
14+
ReportConfigurationUpsertPayload,
15+
} from './report-configuration.types'
16+
17+
@Injectable({ providedIn: 'root' })
18+
export class ReportConfigurationService {
19+
private _errorService = inject(ErrorService)
20+
private _httpClient = inject(HttpClient)
21+
private _reportConfigurations = new BehaviorSubject<ReportConfiguration[]>([])
22+
private _snackBar = inject(SnackBarService)
23+
private _userService = inject(UserService)
24+
25+
reportConfigurations$ = this._reportConfigurations.asObservable()
26+
27+
constructor() {
28+
this._userService.currentOrganizationId$
29+
.pipe(
30+
tap((orgId) => {
31+
this.list(orgId)
32+
}),
33+
)
34+
.subscribe()
35+
}
36+
37+
list(orgId: number) {
38+
const url = `/api/v3/organizations/${orgId}/report_configurations`
39+
this._httpClient
40+
.get<ReportConfigurationsResponse>(url)
41+
.pipe(
42+
map(({ data }) => {
43+
const configs = data.toSorted((a, b) => naturalSort(a.name, b.name))
44+
this._reportConfigurations.next(configs)
45+
return configs
46+
}),
47+
catchError((error: HttpErrorResponse) => {
48+
return this._errorService.handleError(error, 'Error fetching report configurations')
49+
}),
50+
)
51+
.subscribe()
52+
}
53+
54+
create(orgId: number, data: ReportConfigurationUpsertPayload): Observable<ReportConfiguration> {
55+
const url = `/api/v3/report_configurations/?organization_id=${orgId}`
56+
return this._httpClient.post<ReportConfigurationResponse>(url, data).pipe(
57+
map(({ data: config }) => config),
58+
tap(() => {
59+
this.list(orgId)
60+
this._snackBar.success('Created report configuration')
61+
}),
62+
catchError((error: HttpErrorResponse) => {
63+
return this._errorService.handleError(error, 'Error creating report configuration')
64+
}),
65+
)
66+
}
67+
68+
update(orgId: number, id: number, data: ReportConfigurationUpsertPayload): Observable<ReportConfiguration> {
69+
const url = `/api/v3/report_configurations/${id}/?organization_id=${orgId}`
70+
return this._httpClient.put<ReportConfigurationResponse>(url, data).pipe(
71+
map(({ data: config }) => config),
72+
tap(() => {
73+
this.list(orgId)
74+
this._snackBar.success('Updated report configuration')
75+
}),
76+
catchError((error: HttpErrorResponse) => {
77+
return this._errorService.handleError(error, 'Error updating report configuration')
78+
}),
79+
)
80+
}
81+
82+
delete(orgId: number, id: number): Observable<unknown> {
83+
const url = `/api/v3/report_configurations/${id}/?organization_id=${orgId}`
84+
return this._httpClient.delete<unknown>(url).pipe(
85+
tap(() => {
86+
this.list(orgId)
87+
this._snackBar.success('Deleted report configuration')
88+
}),
89+
catchError((error: HttpErrorResponse) => {
90+
return this._errorService.handleError(error, 'Error deleting report configuration')
91+
}),
92+
)
93+
}
94+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export type ReportConfiguration = {
2+
id: number | null;
3+
name: string;
4+
x_column: string | null;
5+
y_column: string | null;
6+
access_level_instance_id: number | null;
7+
access_level_depth: number | null;
8+
cycles: number[];
9+
filter_group_id: number | null;
10+
}
11+
12+
export type ReportConfigurationUpsertPayload = Omit<ReportConfiguration, 'id'>
13+
14+
export type ReportConfigurationsResponse = {
15+
status: string;
16+
data: ReportConfiguration[];
17+
}
18+
19+
export type ReportConfigurationResponse = {
20+
status: string;
21+
data: ReportConfiguration;
22+
}

src/app/core/auth/auth.provider.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import { provideHttpClient, withInterceptors } from '@angular/common/http'
1+
import { provideHttpClient, withInterceptors, withXsrfConfiguration } from '@angular/common/http'
22
import type { EnvironmentProviders, Provider } from '@angular/core'
33
import { inject, provideEnvironmentInitializer } from '@angular/core'
44
import { authInterceptor } from 'app/core/auth/auth.interceptor'
55
import { AuthService } from 'app/core/auth/auth.service'
66

77
export const provideAuth = (): (Provider | EnvironmentProviders)[] => {
8-
return [provideHttpClient(withInterceptors([authInterceptor])), provideEnvironmentInitializer(() => inject(AuthService))]
8+
return [
9+
provideHttpClient(withInterceptors([authInterceptor]), withXsrfConfiguration({ cookieName: 'csrftoken', headerName: 'X-CSRFToken' })),
10+
provideEnvironmentInitializer(() => inject(AuthService)),
11+
]
912
}

0 commit comments

Comments
 (0)