Conversation
There was a problem hiding this comment.
Pull request overview
Ports the AngularJS inventory groups feature into Angular v21, adding a full Group Detail experience (Dashboard, Properties, Systems & Services, Meters, Map) with associated dialogs, an extended GroupsService/types, and assorted fixes to the existing groups list.
Changes:
- New nested route + layout (
group-detail-layout) with sidebar nav, plus child pages for dashboard, properties, systems/services (incl. service detail), meters, and map. - Extended
GroupsServicewithget/getById/getDashboard/getSankeyData/getProperties/getMeters/getMeterUsage, full system/service CRUD, meter update/delete, anduploadMeterReadings; new types ingroups.types.ts. - Updates to existing groups list (cell renderer/click handling, refresh on dialog close) and
form-modal(explicitnext/errorclose handling).
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| src/app/modules/inventory/inventory.routes.ts | Registers nested groups/:groupId routes for the new layout and child pages |
| src/app/modules/inventory-list/groups/modal/form-modal.component.ts | Switches create/edit to explicit next/error handlers |
| src/app/modules/inventory-list/groups/groups.component.ts | New name-cell renderer, navigation handling, refresh-on-modal-close changes |
| src/app/modules/inventory-list/groups/detail/group-detail-layout.{ts,html} | New layout with sidebar navigation menu |
| src/app/modules/inventory-list/groups/detail/dashboard/dashboard.{ts,html} | Group dashboard with cycle/meter-type selectors and sankey list |
| src/app/modules/inventory-list/groups/detail/properties/properties.{ts,html} | AG Grid of group properties |
| src/app/modules/inventory-list/groups/detail/systems/systems.{ts,html} | Systems & Services table with type-specific columns and CRUD wiring |
| src/app/modules/inventory-list/groups/detail/systems/system-dialog/* | Create/edit/delete system dialog |
| src/app/modules/inventory-list/groups/detail/systems/service-dialog/* | Create/edit/delete service dialog |
| src/app/modules/inventory-list/groups/detail/systems/service-detail/* | Service detail page with connected-properties table |
| src/app/modules/inventory-list/groups/detail/systems/dialog-types.ts | Shared dialog data types |
| src/app/modules/inventory-list/groups/detail/meters/meters.{ts,html} | Meters AG Grid + readings panel + cell-action handling |
| src/app/modules/inventory-list/groups/detail/meters/dialogs/edit-meter-dialog.* | Cascading meter-edit dialog |
| src/app/modules/inventory-list/groups/detail/meters/dialogs/delete-meter-dialog.* | Meter delete confirmation dialog |
| src/app/modules/inventory-list/groups/detail/meters/dialogs/upload-readings-dialog.* | 3-step meter readings upload dialog |
| src/app/modules/inventory-list/groups/detail/map/map.{ts,html} | Placeholder map page |
| src/@seed/api/groups/groups.service.ts | Extends API with group-detail/system/service/meter operations |
| src/@seed/api/groups/groups.types.ts | New types for systems/services, dashboard, sankey, properties, meters |
| .spelling.dic | Adds sankey, evse, EVSE to dictionary |
Comments suppressed due to low confidence (1)
src/@seed/api/groups/groups.service.ts:167
get()(here) andgetById()(added at line 159) are two methods that hit the exact same endpoint (/api/v3/inventory_groups/${id}/?organization_id=${orgId}) but interpret the response differently:get()types it asInventoryGroupdirectly, whilegetById()types it asInventoryGroupResponseand unwraps.data. Only one of these can match the actual backend response, so callers of one of them will receive the wrong shape (e.g. an object with{status, data}keys instead of anInventoryGroup, orundefined). The edit-meter dialog usesget()to readgroup.systems, whilegroup-detail-layoutusesgetById()to readgroup.name— at least one of these will be broken at runtime. Please collapse to a single method and confirm whether the endpoint returns the wrapped{status, data}shape or the bare object.
get(orgId: number, id: number): Observable<InventoryGroup> {
const url = `/api/v3/inventory_groups/${id}/?organization_id=${orgId}`
return this._httpClient.get<InventoryGroup>(url).pipe(
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error fetching group')
}),
)
}
update(orgId: number, id: number, data: InventoryGroup): Observable<InventoryGroup> {
const url = `/api/v3/inventory_groups/${id}/?organization_id=${orgId}`
return this._httpClient.put<InventoryGroup>(url, data).pipe(
map((group) => {
this._snackBar.success('Group updated successfully')
this.list(orgId)
return group
}),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error updating group')
}),
)
}
delete(orgId: number, id: number): Observable<unknown> {
const url = `/api/v3/inventory_groups/${id}/?organization_id=${orgId}`
return this._httpClient.delete(url).pipe(
tap(() => {
this._snackBar.success('Group deleted successfully')
this.list(orgId)
}),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error deleting group')
}),
)
}
bulkUpdate(
orgId: number,
addGroupIds: number[],
removeGroupIds: number[],
viewIds: number[],
type: 'property' | 'tax_lot',
): Observable<unknown> {
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')
}),
)
}
getById(orgId: number, groupId: number): Observable<InventoryGroup> {
const url = `/api/v3/inventory_groups/${groupId}/?organization_id=${orgId}`
return this._httpClient.get<InventoryGroupResponse>(url).pipe(
map(({ data }) => data),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error fetching group')
}),
)
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| list(orgId: number) { | ||
| const url = `/api/v3/inventory_groups/?organization_id=${orgId}` | ||
| this._httpClient | ||
| .get<InventoryGroupsResponse>(url) | ||
| .get<InventoryGroup[]>(url) | ||
| .pipe( | ||
| take(1), | ||
| map(({ data }) => { | ||
| this._groups.next(data) | ||
| return data | ||
| map((data) => { | ||
| const groups = Array.isArray(data) ? data : [] | ||
| this._groups.next(groups) | ||
| return groups | ||
| }), | ||
| catchError((error: HttpErrorResponse) => { | ||
| return this._errorService.handleError(error, 'Error fetching groups') | ||
| }), | ||
| ) | ||
| .subscribe() | ||
| } | ||
|
|
||
| // 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<InventoryGroupsResponse>(url, body) | ||
| .pipe( | ||
| take(1), | ||
| map(({ data }) => { | ||
| this._groups.next(data) | ||
| return data | ||
| }), | ||
| catchError((error: HttpErrorResponse) => { | ||
| return this._errorService.handleError(error, 'Error fetching groups for inventory') | ||
| }), | ||
| ) | ||
| .subscribe() | ||
| } | ||
|
|
||
| fetchGroups(orgId: number): Observable<InventoryGroup[]> { | ||
| const url = `/api/v3/inventory_groups/?organization_id=${orgId}` | ||
| return this._httpClient.get<InventoryGroup[]>(url).pipe( | ||
| map((data) => (Array.isArray(data) ? data : [])), | ||
| catchError((error: HttpErrorResponse) => { | ||
| return this._errorService.handleError(error, 'Error fetching groups') | ||
| }), | ||
| ) | ||
| } | ||
|
|
||
| create(orgId: number, data: InventoryGroup): Observable<InventoryGroup> { | ||
| const url = `/api/v3/inventory_groups/?organization_id=${orgId}` | ||
| return this._httpClient.post<InventoryGroupResponse>(url, data).pipe( | ||
| map(({ data }) => { | ||
| return this._httpClient.post<InventoryGroup>(url, data).pipe( | ||
| map((group) => { | ||
| this._snackBar.success('Group created successfully') | ||
| this.list(orgId) | ||
| return data | ||
| return group | ||
| }), | ||
| catchError((error: HttpErrorResponse) => { | ||
| return this._errorService.handleError(error, 'Error updating group') | ||
| }), | ||
| ) | ||
| } | ||
|
|
||
| get(orgId: number, id: number): Observable<InventoryGroup> { | ||
| const url = `/api/v3/inventory_groups/${id}/?organization_id=${orgId}` | ||
| return this._httpClient.get<InventoryGroup>(url).pipe( | ||
| catchError((error: HttpErrorResponse) => { | ||
| return this._errorService.handleError(error, 'Error fetching group') | ||
| }), | ||
| ) | ||
| } | ||
|
|
||
| update(orgId: number, id: number, data: InventoryGroup): Observable<InventoryGroup> { | ||
| const url = `/api/v3/inventory_groups/${id}/?organization_id=${orgId}` | ||
| return this._httpClient.put<InventoryGroupResponse>(url, data).pipe( | ||
| map(({ data }) => { | ||
| return this._httpClient.put<InventoryGroup>(url, data).pipe( | ||
| map((group) => { | ||
| this._snackBar.success('Group updated successfully') | ||
| this.list(orgId) | ||
| return data | ||
| return group | ||
| }), |
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Addressed in 6d5ba86. The groups service now consistently unwraps the backend { status, data } envelope for list/fetch/create/get/update, and I also corrected the create-path error message.
| obs | ||
| .pipe( | ||
| finalize(() => { | ||
| this._dialogRef.close(true) | ||
| }), | ||
| ) | ||
| .subscribe() | ||
| } | ||
|
|
||
| deleteSystem() { | ||
| if (this.submitted) return | ||
| this.submitted = true | ||
| const systemId = this._data.system?.id | ||
| this._groupsService | ||
| .deleteSystem(this._data.orgId, this._data.groupId, systemId) | ||
| .pipe( | ||
| finalize(() => { | ||
| this._dialogRef.close(true) | ||
| }), | ||
| ) | ||
| .subscribe() | ||
| } |
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Addressed in 0b2c26f. The system dialog now uses explicit next/error handlers instead of finalize(), ensuring the dialog only closes on success and users see error feedback on failures.
| obs | ||
| .pipe( | ||
| finalize(() => { | ||
| this._dialogRef.close(true) | ||
| }), | ||
| ) | ||
| .subscribe() | ||
| } | ||
|
|
||
| deleteService() { | ||
| if (this.submitted) return | ||
| this.submitted = true | ||
| const serviceId = this._data.service?.id | ||
| this._groupsService | ||
| .deleteService(this._data.orgId, this._data.groupId, this._data.systemId, serviceId) | ||
| .pipe( | ||
| finalize(() => { | ||
| this._dialogRef.close(true) | ||
| }), | ||
| ) | ||
| .subscribe() |
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Addressed in 0b2c26f. The service dialog now uses explicit next/error handlers instead of finalize(), ensuring the dialog only closes on success and users see error feedback on failures.
| this._organizationService.currentOrganization$ | ||
| .pipe( | ||
| takeUntil(this._unsubscribeAll$), | ||
| take(1), | ||
| tap(({ org_id }) => { | ||
| this.orgId = org_id | ||
| }), | ||
| switchMap(() => this._organizationService.getById(this.orgId)), | ||
| tap((org) => { | ||
| this.cycles = org.cycles | ||
| this.cycleId = org.cycles[0]?.cycle_id | ||
| }), | ||
| switchMap(() => this.loadDashboard()), | ||
| ) | ||
| .subscribe() |
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Addressed in a745a56. Added filter guard filter((org) => org?.org_id != null) to prevent destructuring errors when currentOrganization$ emits null/undefined on initial load.
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| service: any = null | ||
| loading = true | ||
|
|
||
| ngOnInit() { | ||
| this.systemId = parseInt(this._route.snapshot.paramMap.get('systemId')) | ||
| this.serviceId = parseInt(this._route.snapshot.paramMap.get('serviceId')) | ||
|
|
||
| // Walk up to find groupId from parent routes | ||
| let route = this._route.parent | ||
| while (route) { | ||
| const gid = route.snapshot.paramMap.get('groupId') | ||
| if (gid) { | ||
| this.groupId = parseInt(gid) | ||
| break | ||
| } | ||
| route = route.parent | ||
| } | ||
|
|
||
| // Get inventory type from URL | ||
| const urlParts = this._router.url.split('/') | ||
| this.inventoryType = urlParts.find((p) => p === 'properties' || p === 'taxlots') ?? 'properties' |
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Addressed in 4c2f87a. Service detail now uses pathFromRoot to reliably extract inventoryType from route params instead of fragile URL parsing. Added GroupServiceDetail type to replace the any type for the service field, improving type safety.
| changeCycle(cycleId: number) { | ||
| this.cycleId = cycleId | ||
| this.loadDashboard() | ||
| .pipe(switchMap(() => this.loadSankey())) | ||
| .subscribe() | ||
| } | ||
|
|
||
| loadSankey() { | ||
| if (!this.meterType) return this._groupsService.getSankeyData(this.orgId, this.groupId, this.cycleId, '') | ||
| return this._groupsService.getSankeyData(this.orgId, this.groupId, this.cycleId, this.meterType).pipe( | ||
| tap((data) => { | ||
| this.sankeyData = data | ||
| }), | ||
| ) | ||
| } |
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Addressed in cb28df8. The loadSankey() method now resets sankeyData to an empty array and returns of([]) when meterType is empty, preventing stale data from lingering in the UI.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Agent-Logs-Url: https://github.com/SEED-platform/seed-angular/sessions/cca1a734-0ec0-4fd1-b61a-5e77446aaa4c Co-authored-by: kflemin <2205659+kflemin@users.noreply.github.com>
Agent-Logs-Url: https://github.com/SEED-platform/seed-angular/sessions/cca1a734-0ec0-4fd1-b61a-5e77446aaa4c Co-authored-by: kflemin <2205659+kflemin@users.noreply.github.com>
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
…g filter to dashboard Agent-Logs-Url: https://github.com/SEED-platform/seed-angular/sessions/8689fb73-1d01-4af9-b641-d5799b1b7597 Co-authored-by: kflemin <2205659+kflemin@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Agent-Logs-Url: https://github.com/SEED-platform/seed-angular/sessions/8689fb73-1d01-4af9-b641-d5799b1b7597 Co-authored-by: kflemin <2205659+kflemin@users.noreply.github.com>
…ce detail Agent-Logs-Url: https://github.com/SEED-platform/seed-angular/sessions/e129cbf8-e725-4027-aeaa-40b98f7c4c1d Co-authored-by: kflemin <2205659+kflemin@users.noreply.github.com>
Agent-Logs-Url: https://github.com/SEED-platform/seed-angular/sessions/e129cbf8-e725-4027-aeaa-40b98f7c4c1d Co-authored-by: kflemin <2205659+kflemin@users.noreply.github.com>
Adds full inventory group detail functionality, porting the AngularJS inventory groups feature to
Angular v21. Users can navigate into a group to view and manage its dashboard, properties, systems &
services, meters, and map.
Group Detail Layout
Systems & Services
Group Meters
System, Connection, Virtual, Scenario)
Service (scoped to current group)
Properties & Dashboard
API & Types
getServiceDetail methods
service_group, scenario_name, is_virtual
Bug Fixes