From f1f1fc0f505145cfdc8edde43401f7072fbbf9f6 Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Fri, 4 Jul 2025 09:45:29 +0300 Subject: [PATCH 1/5] feat(isntitutions-summary): added structure for fetching data --- .../institutions/institutions.routes.ts | 4 + src/app/features/institutions/models/index.ts | 3 + .../institution-departments-json-api.model.ts | 19 ++ ...ution-index-value-search-json-api.model.ts | 44 ++++ ...titution-summary-metrics-json-api.model.ts | 46 ++++ .../institutions/pages/admin/pages/index.ts | 1 + .../institutions-summary.component.html | 1 + .../institutions-summary.component.scss | 0 .../institutions-summary.component.spec.ts | 22 ++ .../institutions-summary.component.ts | 62 +++++ .../institutions/pages/admin/pages/routes.ts | 14 ++ .../institutions-search.component.ts | 14 +- .../features/institutions/services/index.ts | 1 + .../services/institutions-admin.service.ts | 64 +++++ src/app/features/institutions/store/index.ts | 4 + .../store/institutions-admin.actions.ts | 37 +++ .../store/institutions-admin.model.ts | 13 + .../store/institutions-admin.selectors.ts | 93 +++++++ .../store/institutions-admin.state.ts | 236 ++++++++++++++++++ .../doughnut-chart.component.html | 9 + .../doughnut-chart.component.scss | 12 + .../doughnut-chart.component.spec.ts | 22 ++ .../doughnut-chart.component.ts | 83 ++++++ .../institutions-search.state.ts | 24 +- 24 files changed, 810 insertions(+), 18 deletions(-) create mode 100644 src/app/features/institutions/models/index.ts create mode 100644 src/app/features/institutions/models/institution-departments-json-api.model.ts create mode 100644 src/app/features/institutions/models/institution-index-value-search-json-api.model.ts create mode 100644 src/app/features/institutions/models/institution-summary-metrics-json-api.model.ts create mode 100644 src/app/features/institutions/pages/admin/pages/index.ts create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.html create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.scss create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.spec.ts create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.ts create mode 100644 src/app/features/institutions/pages/admin/pages/routes.ts create mode 100644 src/app/features/institutions/services/index.ts create mode 100644 src/app/features/institutions/services/institutions-admin.service.ts create mode 100644 src/app/features/institutions/store/index.ts create mode 100644 src/app/features/institutions/store/institutions-admin.actions.ts create mode 100644 src/app/features/institutions/store/institutions-admin.model.ts create mode 100644 src/app/features/institutions/store/institutions-admin.selectors.ts create mode 100644 src/app/features/institutions/store/institutions-admin.state.ts create mode 100644 src/app/shared/components/doughnut-chart/doughnut-chart.component.html create mode 100644 src/app/shared/components/doughnut-chart/doughnut-chart.component.scss create mode 100644 src/app/shared/components/doughnut-chart/doughnut-chart.component.spec.ts create mode 100644 src/app/shared/components/doughnut-chart/doughnut-chart.component.ts diff --git a/src/app/features/institutions/institutions.routes.ts b/src/app/features/institutions/institutions.routes.ts index a07caff09..618be2e98 100644 --- a/src/app/features/institutions/institutions.routes.ts +++ b/src/app/features/institutions/institutions.routes.ts @@ -17,4 +17,8 @@ export const routes: Routes = [ component: InstitutionsSearchComponent, providers: [provideStates([InstitutionsSearchState])], }, + { + path: ':institution-id/dashboard', + loadChildren: () => import('./pages/admin/pages/routes').then((inst) => inst.routes), + }, ]; diff --git a/src/app/features/institutions/models/index.ts b/src/app/features/institutions/models/index.ts new file mode 100644 index 000000000..d9b83b6e5 --- /dev/null +++ b/src/app/features/institutions/models/index.ts @@ -0,0 +1,3 @@ +export * from './institution-departments-json-api.model'; +export * from './institution-index-value-search-json-api.model'; +export * from './institution-summary-metrics-json-api.model'; diff --git a/src/app/features/institutions/models/institution-departments-json-api.model.ts b/src/app/features/institutions/models/institution-departments-json-api.model.ts new file mode 100644 index 000000000..6ea8fdf60 --- /dev/null +++ b/src/app/features/institutions/models/institution-departments-json-api.model.ts @@ -0,0 +1,19 @@ +export interface InstitutionDepartmentAttributes { + name: string; + number_of_users: number; +} + +export interface InstitutionDepartmentLinks { + self: string; +} + +export interface InstitutionDepartmentDataJsonAPi { + id: string; + type: 'institution-departments'; + attributes: InstitutionDepartmentAttributes; + links: InstitutionDepartmentLinks; +} + +export interface InstitutionDepartmentsJsonApi { + data: InstitutionDepartmentDataJsonAPi[]; +} diff --git a/src/app/features/institutions/models/institution-index-value-search-json-api.model.ts b/src/app/features/institutions/models/institution-index-value-search-json-api.model.ts new file mode 100644 index 000000000..aa5d475e7 --- /dev/null +++ b/src/app/features/institutions/models/institution-index-value-search-json-api.model.ts @@ -0,0 +1,44 @@ +import { JsonApiResponse } from '@osf/core/models'; + +export interface InstitutionSearchResultCount { + attributes: { + cardSearchResultCount: number; + }; + id: string; + type: 'search-result'; + relationships: { + indexCard: { + data: { + id: string; + type: string; + }; + }; + }; +} + +export interface InstitutionIndexCardFilter { + attributes: { + resourceIdentifier: string[]; + resourceMetadata: { + displayLabel: { '@value': string }[]; + '@id': string; + }; + }; + id: string; + type: 'index-card'; +} + +export type InstitutionIndexValueSearchIncludedJsonApi = InstitutionSearchResultCount | InstitutionIndexCardFilter; + +export interface InstitutionIndexValueSearchJsonApi + extends JsonApiResponse { + data: null; + included: InstitutionIndexValueSearchIncludedJsonApi[]; +} + +export interface InstitutionSearchFilter { + id: string; + label: string; + value: string; + count?: number; +} diff --git a/src/app/features/institutions/models/institution-summary-metrics-json-api.model.ts b/src/app/features/institutions/models/institution-summary-metrics-json-api.model.ts new file mode 100644 index 000000000..62a39243f --- /dev/null +++ b/src/app/features/institutions/models/institution-summary-metrics-json-api.model.ts @@ -0,0 +1,46 @@ +export interface InstitutionSummaryMetricsAttributes { + report_yearmonth: string; + user_count: number; + public_project_count: number; + private_project_count: number; + public_registration_count: number; + embargoed_registration_count: number; + published_preprint_count: number; + public_file_count: number; + storage_byte_count: number; + monthly_logged_in_user_count: number; + monthly_active_user_count: number; +} + +export interface InstitutionSummaryMetricsRelationships { + user: { + data: null; + }; + institution: { + links: { + related: { + href: string; + meta: Record; + }; + }; + data: { + id: string; + type: 'institutions'; + }; + }; +} + +export interface InstitutionSummaryMetricsData { + id: string; + type: 'institution-summary-metrics'; + attributes: InstitutionSummaryMetricsAttributes; + relationships: InstitutionSummaryMetricsRelationships; + links: Record; +} + +export interface InstitutionSummaryMetricsJsonApi { + data: InstitutionSummaryMetricsData; + meta: { + version: string; + }; +} diff --git a/src/app/features/institutions/pages/admin/pages/index.ts b/src/app/features/institutions/pages/admin/pages/index.ts new file mode 100644 index 000000000..cd826f24c --- /dev/null +++ b/src/app/features/institutions/pages/admin/pages/index.ts @@ -0,0 +1 @@ +export { InstitutionsSummaryComponent } from './institutions-summary/institutions-summary.component'; diff --git a/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.html b/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.html new file mode 100644 index 000000000..a463bb8d7 --- /dev/null +++ b/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.html @@ -0,0 +1 @@ +

institutions-summary works!

diff --git a/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.scss b/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.spec.ts b/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.spec.ts new file mode 100644 index 000000000..4a2b17100 --- /dev/null +++ b/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InstitutionsSummaryComponent } from './institutions-summary.component'; + +describe('InstitutionsSummaryComponent', () => { + let component: InstitutionsSummaryComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InstitutionsSummaryComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(InstitutionsSummaryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.ts b/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.ts new file mode 100644 index 000000000..83838bc76 --- /dev/null +++ b/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.ts @@ -0,0 +1,62 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { + FetchHasOsfAddonSearch, + FetchInstitutionDepartments, + FetchInstitutionSummaryMetrics, + FetchStorageRegionSearch, + InstitutionsAdminSelectors, + SetSelectedInstitutionId, +} from '../../../../store'; + +@Component({ + selector: 'osf-institutions-summary', + imports: [], + templateUrl: './institutions-summary.component.html', + styleUrl: './institutions-summary.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class InstitutionsSummaryComponent implements OnInit { + private readonly route = inject(ActivatedRoute); + + departments = select(InstitutionsAdminSelectors.getDepartments); + departmentsLoading = select(InstitutionsAdminSelectors.getDepartmentsLoading); + departmentsError = select(InstitutionsAdminSelectors.getDepartmentsError); + + summaryMetrics = select(InstitutionsAdminSelectors.getSummaryMetrics); + summaryMetricsLoading = select(InstitutionsAdminSelectors.getSummaryMetricsLoading); + summaryMetricsError = select(InstitutionsAdminSelectors.getSummaryMetricsError); + + hasOsfAddonSearch = select(InstitutionsAdminSelectors.getHasOsfAddonSearch); + hasOsfAddonSearchLoading = select(InstitutionsAdminSelectors.getHasOsfAddonSearchLoading); + hasOsfAddonSearchError = select(InstitutionsAdminSelectors.getHasOsfAddonSearchError); + + storageRegionSearch = select(InstitutionsAdminSelectors.getStorageRegionSearch); + storageRegionSearchLoading = select(InstitutionsAdminSelectors.getStorageRegionSearchLoading); + storageRegionSearchError = select(InstitutionsAdminSelectors.getStorageRegionSearchError); + + selectedInstitutionId = select(InstitutionsAdminSelectors.getSelectedInstitutionId); + + private readonly actions = createDispatchMap({ + fetchDepartments: FetchInstitutionDepartments, + fetchSummaryMetrics: FetchInstitutionSummaryMetrics, + fetchHasOsfAddonSearch: FetchHasOsfAddonSearch, + fetchStorageRegionSearch: FetchStorageRegionSearch, + setSelectedInstitutionId: SetSelectedInstitutionId, + }); + + ngOnInit(): void { + const institutionId = this.route.snapshot.params['institution-id']; + + if (institutionId) { + this.actions.setSelectedInstitutionId(institutionId); + this.actions.fetchDepartments(institutionId); + this.actions.fetchSummaryMetrics(institutionId); + this.actions.fetchHasOsfAddonSearch(institutionId); + this.actions.fetchStorageRegionSearch(institutionId); + } + } +} diff --git a/src/app/features/institutions/pages/admin/pages/routes.ts b/src/app/features/institutions/pages/admin/pages/routes.ts new file mode 100644 index 000000000..99aa60117 --- /dev/null +++ b/src/app/features/institutions/pages/admin/pages/routes.ts @@ -0,0 +1,14 @@ +import { provideStates } from '@ngxs/store'; + +import { Routes } from '@angular/router'; + +import { InstitutionsSummaryComponent } from '@osf/features/institutions/pages/admin/pages'; +import { InstitutionsAdminState } from '@osf/features/institutions/store'; + +export const routes: Routes = [ + { + path: '', + component: InstitutionsSummaryComponent, + providers: [provideStates([InstitutionsAdminState])], + }, +]; diff --git a/src/app/features/institutions/pages/institutions-search/institutions-search.component.ts b/src/app/features/institutions/pages/institutions-search/institutions-search.component.ts index 5fd29abe5..0b907846a 100644 --- a/src/app/features/institutions/pages/institutions-search/institutions-search.component.ts +++ b/src/app/features/institutions/pages/institutions-search/institutions-search.component.ts @@ -15,16 +15,16 @@ import { FormControl, FormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { + FilterChipsComponent, LoadingSpinnerComponent, ReusableFilterComponent, SearchHelpTutorialComponent, SearchInputComponent, -} from '@shared/components'; -import { FilterChipsComponent } from '@shared/components/filter-chips/filter-chips.component'; -import { SearchResultsContainerComponent } from '@shared/components/search-results-container/search-results-container.component'; -import { SEARCH_TAB_OPTIONS } from '@shared/constants'; -import { ResourceTab } from '@shared/enums'; -import { DiscoverableFilter } from '@shared/models'; + SearchResultsContainerComponent, +} from '@osf/shared/components'; +import { SEARCH_TAB_OPTIONS } from '@osf/shared/constants'; +import { ResourceTab } from '@osf/shared/enums'; +import { DiscoverableFilter } from '@osf/shared/models'; import { FetchInstitutionById, FetchResources, @@ -36,7 +36,7 @@ import { UpdateFilterValue, UpdateResourceType, UpdateSortBy, -} from '@shared/stores'; +} from '@osf/shared/stores'; @Component({ selector: 'osf-institutions-search', diff --git a/src/app/features/institutions/services/index.ts b/src/app/features/institutions/services/index.ts new file mode 100644 index 000000000..6febec8a5 --- /dev/null +++ b/src/app/features/institutions/services/index.ts @@ -0,0 +1 @@ +export * from './institutions-admin.service'; diff --git a/src/app/features/institutions/services/institutions-admin.service.ts b/src/app/features/institutions/services/institutions-admin.service.ts new file mode 100644 index 000000000..e9b978aa8 --- /dev/null +++ b/src/app/features/institutions/services/institutions-admin.service.ts @@ -0,0 +1,64 @@ +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiService } from '@osf/core/services'; + +import { + InstitutionDepartmentsJsonApi, + InstitutionIndexCardFilter, + InstitutionIndexValueSearchJsonApi, + InstitutionSearchFilter, + InstitutionSummaryMetricsJsonApi, +} from '../models'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class InstitutionsAdminService { + private jsonApiService = inject(JsonApiService); + + fetchDepartments(institutionId: string): Observable { + return this.jsonApiService.get( + `${environment.apiUrl}/institutions/${institutionId}/metrics/departments/` + ); + } + + fetchSummary(institutionId: string): Observable { + return this.jsonApiService.get( + `${environment.apiUrl}/institutions/${institutionId}/metrics/summary/` + ); + } + + fetchIndexValueSearch( + institutionId: string, + valueSearchPropertyPath: string, + additionalParams?: Record + ): Observable { + const params: Record = { + 'cardSearchFilter[affiliation]': `https://ror.org/05d5mza29,https://test.osf.io/institutions/${institutionId}/`, + valueSearchPropertyPath, + 'page[size]': '10', + ...additionalParams, + }; + + return this.jsonApiService + .get(`${environment.shareDomainUrl}/index-value-search`, params) + .pipe( + map((response) => { + if (response?.included) { + return response.included + .filter((item): item is InstitutionIndexCardFilter => item.type === 'index-card') + .map((item) => ({ + id: item.id, + label: item.attributes?.resourceMetadata?.displayLabel?.[0]?.['@value'] || item.id, + value: item.attributes?.resourceMetadata?.['@id'] || item.id, + })); + } + return []; + }) + ); + } +} diff --git a/src/app/features/institutions/store/index.ts b/src/app/features/institutions/store/index.ts new file mode 100644 index 000000000..9c942fc07 --- /dev/null +++ b/src/app/features/institutions/store/index.ts @@ -0,0 +1,4 @@ +export * from './institutions-admin.actions'; +export * from './institutions-admin.model'; +export * from './institutions-admin.selectors'; +export * from './institutions-admin.state'; diff --git a/src/app/features/institutions/store/institutions-admin.actions.ts b/src/app/features/institutions/store/institutions-admin.actions.ts new file mode 100644 index 000000000..55cf51fb6 --- /dev/null +++ b/src/app/features/institutions/store/institutions-admin.actions.ts @@ -0,0 +1,37 @@ +export class FetchInstitutionDepartments { + static readonly type = '[InstitutionsAdmin] Fetch Institution Departments'; + constructor(public institutionId: string) {} +} + +export class FetchInstitutionSummaryMetrics { + static readonly type = '[InstitutionsAdmin] Fetch Institution Summary Metrics'; + constructor(public institutionId: string) {} +} + +export class FetchInstitutionSearchResults { + static readonly type = '[InstitutionsAdmin] Fetch Institution Search Results'; + constructor( + public institutionId: string, + public valueSearchPropertyPath: string, + public additionalParams?: Record + ) {} +} + +export class FetchHasOsfAddonSearch { + static readonly type = '[InstitutionsAdmin] Fetch Has OSF Addon Search'; + constructor(public institutionId: string) {} +} + +export class FetchStorageRegionSearch { + static readonly type = '[InstitutionsAdmin] Fetch Storage Region Search'; + constructor(public institutionId: string) {} +} + +export class SetSelectedInstitutionId { + static readonly type = '[InstitutionsAdmin] Set Selected Institution Id'; + constructor(public institutionId: string) {} +} + +export class ClearInstitutionsAdminData { + static readonly type = '[InstitutionsAdmin] Clear Data'; +} diff --git a/src/app/features/institutions/store/institutions-admin.model.ts b/src/app/features/institutions/store/institutions-admin.model.ts new file mode 100644 index 000000000..0d8d14fd2 --- /dev/null +++ b/src/app/features/institutions/store/institutions-admin.model.ts @@ -0,0 +1,13 @@ +import { AsyncStateModel } from '@shared/models'; + +import { InstitutionDepartmentsJsonApi, InstitutionSearchFilter, InstitutionSummaryMetricsJsonApi } from '../models'; + +export interface InstitutionsAdminModel { + departments: AsyncStateModel; + summaryMetrics: AsyncStateModel; + hasOsfAddonSearch: AsyncStateModel; + storageRegionSearch: AsyncStateModel; + searchResults: AsyncStateModel; + selectedInstitutionId: string | null; + currentSearchPropertyPath: string | null; +} diff --git a/src/app/features/institutions/store/institutions-admin.selectors.ts b/src/app/features/institutions/store/institutions-admin.selectors.ts new file mode 100644 index 000000000..359f36a6e --- /dev/null +++ b/src/app/features/institutions/store/institutions-admin.selectors.ts @@ -0,0 +1,93 @@ +import { Selector } from '@ngxs/store'; + +import { InstitutionDepartmentsJsonApi, InstitutionSearchFilter, InstitutionSummaryMetricsJsonApi } from '../models'; + +import { InstitutionsAdminModel } from './institutions-admin.model'; +import { InstitutionsAdminState } from './institutions-admin.state'; + +export class InstitutionsAdminSelectors { + @Selector([InstitutionsAdminState]) + static getDepartments(state: InstitutionsAdminModel): InstitutionDepartmentsJsonApi { + return state.departments.data; + } + + @Selector([InstitutionsAdminState]) + static getDepartmentsLoading(state: InstitutionsAdminModel): boolean { + return state.departments.isLoading; + } + + @Selector([InstitutionsAdminState]) + static getDepartmentsError(state: InstitutionsAdminModel): string | null { + return state.departments.error; + } + + @Selector([InstitutionsAdminState]) + static getSummaryMetrics(state: InstitutionsAdminModel): InstitutionSummaryMetricsJsonApi { + return state.summaryMetrics.data; + } + + @Selector([InstitutionsAdminState]) + static getSummaryMetricsLoading(state: InstitutionsAdminModel): boolean { + return state.summaryMetrics.isLoading; + } + + @Selector([InstitutionsAdminState]) + static getSummaryMetricsError(state: InstitutionsAdminModel): string | null { + return state.summaryMetrics.error; + } + + @Selector([InstitutionsAdminState]) + static getHasOsfAddonSearch(state: InstitutionsAdminModel): InstitutionSearchFilter[] { + return state.hasOsfAddonSearch.data; + } + + @Selector([InstitutionsAdminState]) + static getHasOsfAddonSearchLoading(state: InstitutionsAdminModel): boolean { + return state.hasOsfAddonSearch.isLoading; + } + + @Selector([InstitutionsAdminState]) + static getHasOsfAddonSearchError(state: InstitutionsAdminModel): string | null { + return state.hasOsfAddonSearch.error; + } + + @Selector([InstitutionsAdminState]) + static getStorageRegionSearch(state: InstitutionsAdminModel): InstitutionSearchFilter[] { + return state.storageRegionSearch.data; + } + + @Selector([InstitutionsAdminState]) + static getStorageRegionSearchLoading(state: InstitutionsAdminModel): boolean { + return state.storageRegionSearch.isLoading; + } + + @Selector([InstitutionsAdminState]) + static getStorageRegionSearchError(state: InstitutionsAdminModel): string | null { + return state.storageRegionSearch.error; + } + + @Selector([InstitutionsAdminState]) + static getSearchResults(state: InstitutionsAdminModel): InstitutionSearchFilter[] { + return state.searchResults.data; + } + + @Selector([InstitutionsAdminState]) + static getSearchResultsLoading(state: InstitutionsAdminModel): boolean { + return state.searchResults.isLoading; + } + + @Selector([InstitutionsAdminState]) + static getSearchResultsError(state: InstitutionsAdminModel): string | null { + return state.searchResults.error; + } + + @Selector([InstitutionsAdminState]) + static getSelectedInstitutionId(state: InstitutionsAdminModel): string | null { + return state.selectedInstitutionId; + } + + @Selector([InstitutionsAdminState]) + static getCurrentSearchPropertyPath(state: InstitutionsAdminModel): string | null { + return state.currentSearchPropertyPath; + } +} diff --git a/src/app/features/institutions/store/institutions-admin.state.ts b/src/app/features/institutions/store/institutions-admin.state.ts new file mode 100644 index 000000000..178c8cfc5 --- /dev/null +++ b/src/app/features/institutions/store/institutions-admin.state.ts @@ -0,0 +1,236 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { catchError, tap, throwError } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { InstitutionDepartmentsJsonApi, InstitutionSearchFilter, InstitutionSummaryMetricsJsonApi } from '../models'; +import { InstitutionsAdminService } from '../services/institutions-admin.service'; + +import { + ClearInstitutionsAdminData, + FetchHasOsfAddonSearch, + FetchInstitutionDepartments, + FetchInstitutionSearchResults, + FetchInstitutionSummaryMetrics, + FetchStorageRegionSearch, + SetSelectedInstitutionId, +} from './institutions-admin.actions'; +import { InstitutionsAdminModel } from './institutions-admin.model'; + +const createEmptyDepartments = (): InstitutionDepartmentsJsonApi => ({ + data: [], +}); + +const createEmptySummaryMetrics = (): InstitutionSummaryMetricsJsonApi => ({ + data: { + id: '', + type: 'institution-summary-metrics', + attributes: { + report_yearmonth: '', + user_count: 0, + public_project_count: 0, + private_project_count: 0, + public_registration_count: 0, + embargoed_registration_count: 0, + published_preprint_count: 0, + public_file_count: 0, + storage_byte_count: 0, + monthly_logged_in_user_count: 0, + monthly_active_user_count: 0, + }, + relationships: { + user: { + data: null, + }, + institution: { + links: { + related: { + href: '', + meta: {}, + }, + }, + data: { + id: '', + type: 'institutions', + }, + }, + }, + links: {}, + }, + meta: { + version: '', + }, +}); + +const createEmptySearchFilters = (): InstitutionSearchFilter[] => []; + +@State({ + name: 'institutionsAdmin', + defaults: { + departments: { data: createEmptyDepartments(), isLoading: false, error: null }, + summaryMetrics: { data: createEmptySummaryMetrics(), isLoading: false, error: null }, + hasOsfAddonSearch: { data: createEmptySearchFilters(), isLoading: false, error: null }, + storageRegionSearch: { data: createEmptySearchFilters(), isLoading: false, error: null }, + searchResults: { data: createEmptySearchFilters(), isLoading: false, error: null }, + selectedInstitutionId: null, + currentSearchPropertyPath: null, + }, +}) +@Injectable() +export class InstitutionsAdminState { + private readonly institutionsAdminService = inject(InstitutionsAdminService); + + @Action(FetchInstitutionDepartments) + fetchDepartments(ctx: StateContext, action: FetchInstitutionDepartments) { + const state = ctx.getState(); + ctx.patchState({ + departments: { ...state.departments, isLoading: true, error: null }, + }); + + return this.institutionsAdminService.fetchDepartments(action.institutionId).pipe( + tap((response) => { + ctx.patchState({ + departments: { data: response, isLoading: false, error: null }, + }); + }), + catchError((error) => { + ctx.patchState({ + departments: { + ...state.departments, + isLoading: false, + error: error.message || 'Failed to fetch departments', + }, + }); + return throwError(() => error); + }) + ); + } + + @Action(FetchInstitutionSummaryMetrics) + fetchSummaryMetrics(ctx: StateContext, action: FetchInstitutionSummaryMetrics) { + const state = ctx.getState(); + ctx.patchState({ + summaryMetrics: { ...state.summaryMetrics, isLoading: true, error: null }, + }); + + return this.institutionsAdminService.fetchSummary(action.institutionId).pipe( + tap((response) => { + ctx.patchState({ + summaryMetrics: { data: response, isLoading: false, error: null }, + }); + }), + catchError((error) => { + ctx.patchState({ + summaryMetrics: { + ...state.summaryMetrics, + isLoading: false, + error: error.message || 'Failed to fetch summary metrics', + }, + }); + return throwError(() => error); + }) + ); + } + + @Action(FetchInstitutionSearchResults) + fetchSearchResults(ctx: StateContext, action: FetchInstitutionSearchResults) { + const state = ctx.getState(); + ctx.patchState({ + searchResults: { ...state.searchResults, isLoading: true, error: null }, + currentSearchPropertyPath: action.valueSearchPropertyPath, + }); + + return this.institutionsAdminService + .fetchIndexValueSearch(action.institutionId, action.valueSearchPropertyPath, action.additionalParams) + .pipe( + tap((response) => { + ctx.patchState({ + searchResults: { data: response, isLoading: false, error: null }, + }); + }), + catchError((error) => { + ctx.patchState({ + searchResults: { + ...state.searchResults, + isLoading: false, + error: error.message || 'Failed to fetch search results', + }, + }); + return throwError(() => error); + }) + ); + } + + @Action(FetchHasOsfAddonSearch) + fetchHasOsfAddonSearch(ctx: StateContext, action: FetchHasOsfAddonSearch) { + const state = ctx.getState(); + ctx.patchState({ + hasOsfAddonSearch: { ...state.hasOsfAddonSearch, isLoading: true, error: null }, + }); + + return this.institutionsAdminService.fetchIndexValueSearch(action.institutionId, 'hasOsfAddon').pipe( + tap((response) => { + ctx.patchState({ + hasOsfAddonSearch: { data: response, isLoading: false, error: null }, + }); + }), + catchError((error) => { + ctx.patchState({ + hasOsfAddonSearch: { + ...state.hasOsfAddonSearch, + isLoading: false, + error: error.message || 'Failed to fetch has OSF addon search', + }, + }); + return throwError(() => error); + }) + ); + } + + @Action(FetchStorageRegionSearch) + fetchStorageRegionSearch(ctx: StateContext, action: FetchStorageRegionSearch) { + const state = ctx.getState(); + ctx.patchState({ + storageRegionSearch: { ...state.storageRegionSearch, isLoading: true, error: null }, + }); + + return this.institutionsAdminService.fetchIndexValueSearch(action.institutionId, 'storageRegion').pipe( + tap((response) => { + ctx.patchState({ + storageRegionSearch: { data: response, isLoading: false, error: null }, + }); + }), + catchError((error) => { + ctx.patchState({ + storageRegionSearch: { + ...state.storageRegionSearch, + isLoading: false, + error: error.message || 'Failed to fetch storage region search', + }, + }); + return throwError(() => error); + }) + ); + } + + @Action(SetSelectedInstitutionId) + setSelectedInstitutionId(ctx: StateContext, action: SetSelectedInstitutionId) { + ctx.patchState({ + selectedInstitutionId: action.institutionId, + }); + } + + @Action(ClearInstitutionsAdminData) + clearData(ctx: StateContext) { + ctx.patchState({ + departments: { data: createEmptyDepartments(), isLoading: false, error: null }, + summaryMetrics: { data: createEmptySummaryMetrics(), isLoading: false, error: null }, + hasOsfAddonSearch: { data: createEmptySearchFilters(), isLoading: false, error: null }, + storageRegionSearch: { data: createEmptySearchFilters(), isLoading: false, error: null }, + searchResults: { data: createEmptySearchFilters(), isLoading: false, error: null }, + selectedInstitutionId: null, + currentSearchPropertyPath: null, + }); + } +} diff --git a/src/app/shared/components/doughnut-chart/doughnut-chart.component.html b/src/app/shared/components/doughnut-chart/doughnut-chart.component.html new file mode 100644 index 000000000..efa813004 --- /dev/null +++ b/src/app/shared/components/doughnut-chart/doughnut-chart.component.html @@ -0,0 +1,9 @@ +

{{ title() | translate }}

+ +@if (isLoading()) { + +} @else { +
+ +
+} diff --git a/src/app/shared/components/doughnut-chart/doughnut-chart.component.scss b/src/app/shared/components/doughnut-chart/doughnut-chart.component.scss new file mode 100644 index 000000000..2061ecc7a --- /dev/null +++ b/src/app/shared/components/doughnut-chart/doughnut-chart.component.scss @@ -0,0 +1,12 @@ +:host { + display: block; +} + +.chart-title { + font-size: 1.7rem; +} + +.chart { + display: block; + margin-top: 1.7rem; +} diff --git a/src/app/shared/components/doughnut-chart/doughnut-chart.component.spec.ts b/src/app/shared/components/doughnut-chart/doughnut-chart.component.spec.ts new file mode 100644 index 000000000..0e8406fba --- /dev/null +++ b/src/app/shared/components/doughnut-chart/doughnut-chart.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DoughnutChartComponent } from './doughnut-chart.component'; + +describe('PieChartComponent', () => { + let component: DoughnutChartComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DoughnutChartComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DoughnutChartComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/doughnut-chart/doughnut-chart.component.ts b/src/app/shared/components/doughnut-chart/doughnut-chart.component.ts new file mode 100644 index 000000000..e185ba496 --- /dev/null +++ b/src/app/shared/components/doughnut-chart/doughnut-chart.component.ts @@ -0,0 +1,83 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { ChartModule } from 'primeng/chart'; + +import { isPlatformBrowser } from '@angular/common'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject, + input, + OnInit, + PLATFORM_ID, + signal, +} from '@angular/core'; + +import { DatasetInput } from '@osf/shared/models'; +import { PIE_CHART_PALETTE } from '@osf/shared/utils'; + +import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.component'; + +import { ChartData, ChartOptions } from 'chart.js'; + +@Component({ + selector: 'osf-pie-chart', + imports: [ChartModule, TranslatePipe, LoadingSpinnerComponent], + templateUrl: './doughnut-chart.component.html', + styleUrl: './doughnut-chart.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DoughnutChartComponent implements OnInit { + isLoading = input(false); + title = input(''); + labels = input([]); + datasets = input([]); + showLegend = input(false); + + protected options = signal({}); + protected data = signal({} as ChartData); + + #platformId = inject(PLATFORM_ID); + #cd = inject(ChangeDetectorRef); + + ngOnInit() { + this.initChart(); + } + + initChart() { + if (isPlatformBrowser(this.#platformId)) { + this.setChartData(); + this.setChartOptions(); + + this.#cd.markForCheck(); + } + } + + private setChartData() { + const chartDatasets = this.datasets().map((dataset) => ({ + label: dataset.label, + data: dataset.data, + backgroundColor: PIE_CHART_PALETTE, + borderWidth: 0, + })); + + this.data.set({ + labels: this.labels(), + datasets: chartDatasets, + }); + } + + private setChartOptions() { + this.options.set({ + maintainAspectRatio: true, + responsive: true, + plugins: { + legend: { + display: this.showLegend(), + position: 'bottom', + }, + }, + }); + } +} diff --git a/src/app/shared/stores/institutions-search/institutions-search.state.ts b/src/app/shared/stores/institutions-search/institutions-search.state.ts index a5ae21ac4..d20693065 100644 --- a/src/app/shared/stores/institutions-search/institutions-search.state.ts +++ b/src/app/shared/stores/institutions-search/institutions-search.state.ts @@ -6,18 +6,20 @@ import { BehaviorSubject, catchError, EMPTY, forkJoin, of, switchMap, tap, throw import { inject, Injectable } from '@angular/core'; import { ResourcesData } from '@osf/features/search/models'; -import { GetResourcesRequestTypeEnum, ResourceTab } from '@shared/enums'; -import { Institution } from '@shared/models'; -import { InstitutionsService, SearchService } from '@shared/services'; -import { FetchResources, FetchResourcesByLink, InstitutionsSearchSelectors, UpdateResourceType } from '@shared/stores'; -import { getResourceTypes } from '@shared/utils'; +import { GetResourcesRequestTypeEnum, ResourceTab } from '@osf/shared/enums'; +import { Institution } from '@osf/shared/models'; +import { InstitutionsService, SearchService } from '@osf/shared/services'; +import { getResourceTypes } from '@osf/shared/utils'; import { FetchInstitutionById, + FetchResources, + FetchResourcesByLink, LoadFilterOptions, LoadFilterOptionsAndSetValues, SetFilterValues, UpdateFilterValue, + UpdateResourceType, UpdateSortBy, } from './institutions-search.actions'; import { InstitutionsSearchModel } from './institutions-search.model'; @@ -71,12 +73,12 @@ export class InstitutionsSearchState implements NgxsOnInit { const state = ctx.getState(); ctx.patchState({ resources: { ...state.resources, isLoading: true } }); const filtersParams: Record = {}; - const searchText = this.store.selectSnapshot(InstitutionsSearchSelectors.getSearchText); - const sortBy = this.store.selectSnapshot(InstitutionsSearchSelectors.getSortBy); - const resourceTab = this.store.selectSnapshot(InstitutionsSearchSelectors.getResourceType); + const searchText = state.searchText; + const sortBy = state.sortBy; + const resourceTab = state.resourceType; const resourceTypes = getResourceTypes(resourceTab); - filtersParams['cardSearchFilter[affiliation][]'] = this.store.selectSnapshot(InstitutionsSearchSelectors.getIris); + filtersParams['cardSearchFilter[affiliation][]'] = state.providerIri; Object.entries(state.filterValues).forEach(([key, value]) => { if (value) filtersParams[`cardSearchFilter[${key}][]`] = value; @@ -148,8 +150,8 @@ export class InstitutionsSearchState implements NgxsOnInit { } @Action(FetchResources) - getResources() { - if (!this.store.selectSnapshot(InstitutionsSearchSelectors.getIris)) return; + getResources(ctx: StateContext) { + if (!ctx.getState().providerIri) return; this.loadRequests.next({ type: GetResourcesRequestTypeEnum.GetResources }); } From 70e366af0277db65d62b11573ea6355479cae845 Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Mon, 7 Jul 2025 13:22:54 +0300 Subject: [PATCH 2/5] feat(isntitutions-summary): added summary page and admin structure --- .../institutions/institutions.routes.ts | 2 +- .../features/institutions/mappers/index.ts | 1 + .../institution-summary-index.mapper.ts | 40 +++++ .../institution-summary-metrics.mapper.ts | 19 +++ src/app/features/institutions/models/index.ts | 1 + ...ution-index-value-search-json-api.model.ts | 7 +- .../institution-summary-metric.model.ts | 13 ++ .../admin/admin-institutions.component.html | 32 ++++ .../admin/admin-institutions.component.scss | 5 + .../admin-institutions.component.spec.ts | 22 +++ .../admin/admin-institutions.component.ts | 67 ++++++++ .../institutions-preprints.component.html | 0 .../institutions-preprints.component.scss | 0 .../institutions-preprints.component.spec.ts | 22 +++ .../institutions-preprints.component.ts | 10 ++ .../institutions-projects.component.html | 0 .../institutions-projects.component.scss | 0 .../institutions-projects.component.spec.ts | 22 +++ .../institutions-projects.component.ts | 10 ++ .../institutions-registrations.component.html | 0 .../institutions-registrations.component.scss | 0 ...stitutions-registrations.component.spec.ts | 22 +++ .../institutions-registrations.component.ts | 10 ++ .../institutions-summary.component.html | 90 +++++++++- .../institutions-summary.component.scss | 15 ++ .../institutions-summary.component.ts | 156 ++++++++++++++++-- .../institutions-users.component.html | 4 + .../institutions-users.component.scss | 0 .../institutions-users.component.spec.ts | 22 +++ .../institutions-users.component.ts | 10 ++ .../institutions/pages/admin/pages/routes.ts | 14 -- .../institutions/pages/admin/routes.ts | 46 ++++++ .../services/institutions-admin.service.ts | 135 ++++++++++++--- .../store/institutions-admin.model.ts | 4 +- .../store/institutions-admin.selectors.ts | 4 +- .../store/institutions-admin.state.ts | 41 ++--- .../bar-chart/bar-chart.component.html | 32 +++- .../bar-chart/bar-chart.component.ts | 22 ++- .../doughnut-chart.component.html | 32 +++- .../doughnut-chart.component.scss | 1 + .../doughnut-chart.component.ts | 31 +++- src/app/shared/components/index.ts | 1 + .../statistic-card.component.html | 6 + .../statistic-card.component.scss | 7 + .../statistic-card.component.spec.ts | 22 +++ .../statistic-card.component.ts | 15 ++ .../institutions-search.state.ts | 3 +- src/assets/i18n/en.json | 21 +++ 48 files changed, 936 insertions(+), 103 deletions(-) create mode 100644 src/app/features/institutions/mappers/index.ts create mode 100644 src/app/features/institutions/mappers/institution-summary-index.mapper.ts create mode 100644 src/app/features/institutions/mappers/institution-summary-metrics.mapper.ts create mode 100644 src/app/features/institutions/models/institution-summary-metric.model.ts create mode 100644 src/app/features/institutions/pages/admin/admin-institutions.component.html create mode 100644 src/app/features/institutions/pages/admin/admin-institutions.component.scss create mode 100644 src/app/features/institutions/pages/admin/admin-institutions.component.spec.ts create mode 100644 src/app/features/institutions/pages/admin/admin-institutions.component.ts create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.html create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.scss create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.spec.ts create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.ts create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.html create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.scss create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.spec.ts create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.ts create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.html create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.scss create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.spec.ts create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.ts create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.html create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.scss create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.spec.ts create mode 100644 src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.ts delete mode 100644 src/app/features/institutions/pages/admin/pages/routes.ts create mode 100644 src/app/features/institutions/pages/admin/routes.ts create mode 100644 src/app/shared/components/statistic-card/statistic-card.component.html create mode 100644 src/app/shared/components/statistic-card/statistic-card.component.scss create mode 100644 src/app/shared/components/statistic-card/statistic-card.component.spec.ts create mode 100644 src/app/shared/components/statistic-card/statistic-card.component.ts diff --git a/src/app/features/institutions/institutions.routes.ts b/src/app/features/institutions/institutions.routes.ts index 618be2e98..44082d7e8 100644 --- a/src/app/features/institutions/institutions.routes.ts +++ b/src/app/features/institutions/institutions.routes.ts @@ -19,6 +19,6 @@ export const routes: Routes = [ }, { path: ':institution-id/dashboard', - loadChildren: () => import('./pages/admin/pages/routes').then((inst) => inst.routes), + loadChildren: () => import('./pages/admin/routes').then((inst) => inst.routes), }, ]; diff --git a/src/app/features/institutions/mappers/index.ts b/src/app/features/institutions/mappers/index.ts new file mode 100644 index 000000000..9d97e869f --- /dev/null +++ b/src/app/features/institutions/mappers/index.ts @@ -0,0 +1 @@ +export { mapInstitutionSummaryMetrics } from './institution-summary-metrics.mapper'; diff --git a/src/app/features/institutions/mappers/institution-summary-index.mapper.ts b/src/app/features/institutions/mappers/institution-summary-index.mapper.ts new file mode 100644 index 000000000..5560342f2 --- /dev/null +++ b/src/app/features/institutions/mappers/institution-summary-index.mapper.ts @@ -0,0 +1,40 @@ +import { + InstitutionIndexCardFilter, + InstitutionIndexValueSearchIncludedJsonApi, + InstitutionSearchFilter, + InstitutionSearchResultCount, +} from '@osf/features/institutions/models'; + +export function mapIndexCardResults(included: InstitutionIndexValueSearchIncludedJsonApi[]): InstitutionSearchFilter[] { + const indexCardMap = included.reduce( + (acc, item) => { + if (item.type === 'index-card') { + acc[item.id] = { + id: item.id, + label: + (item as InstitutionIndexCardFilter)?.attributes?.resourceMetadata?.displayLabel?.[0]?.['@value'] || + (item as InstitutionIndexCardFilter)?.attributes?.resourceMetadata?.name?.[0]?.['@value'] || + item.id, + }; + } + return acc; + }, + {} as Record + ); + + return included.reduce((result, item) => { + if (item.type === 'search-result') { + const indexCardId = (item as InstitutionSearchResultCount).relationships?.indexCard?.data?.id; + const count = (item as InstitutionSearchResultCount).attributes?.cardSearchResultCount ?? 0; + const indexCard = indexCardMap[indexCardId]; + if (indexCard) { + result.push({ + id: indexCard.id, + label: indexCard.label, + value: count, + } as InstitutionSearchFilter); + } + } + return result; + }, [] as InstitutionSearchFilter[]); +} diff --git a/src/app/features/institutions/mappers/institution-summary-metrics.mapper.ts b/src/app/features/institutions/mappers/institution-summary-metrics.mapper.ts new file mode 100644 index 000000000..eb808e4b2 --- /dev/null +++ b/src/app/features/institutions/mappers/institution-summary-metrics.mapper.ts @@ -0,0 +1,19 @@ +import { InstitutionSummaryMetrics, InstitutionSummaryMetricsAttributes } from '@osf/features/institutions/models'; + +export function mapInstitutionSummaryMetrics( + attributes: InstitutionSummaryMetricsAttributes +): InstitutionSummaryMetrics { + return { + reportYearmonth: attributes.report_yearmonth, + userCount: attributes.user_count, + publicProjectCount: attributes.public_project_count, + privateProjectCount: attributes.private_project_count, + publicRegistrationCount: attributes.public_registration_count, + embargoedRegistrationCount: attributes.embargoed_registration_count, + publishedPreprintCount: attributes.published_preprint_count, + publicFileCount: attributes.public_file_count, + storageByteCount: attributes.storage_byte_count, + monthlyLoggedInUserCount: attributes.monthly_logged_in_user_count, + monthlyActiveUserCount: attributes.monthly_active_user_count, + }; +} diff --git a/src/app/features/institutions/models/index.ts b/src/app/features/institutions/models/index.ts index d9b83b6e5..5f23583a9 100644 --- a/src/app/features/institutions/models/index.ts +++ b/src/app/features/institutions/models/index.ts @@ -1,3 +1,4 @@ export * from './institution-departments-json-api.model'; export * from './institution-index-value-search-json-api.model'; +export * from './institution-summary-metric.model'; export * from './institution-summary-metrics-json-api.model'; diff --git a/src/app/features/institutions/models/institution-index-value-search-json-api.model.ts b/src/app/features/institutions/models/institution-index-value-search-json-api.model.ts index aa5d475e7..af48c27f7 100644 --- a/src/app/features/institutions/models/institution-index-value-search-json-api.model.ts +++ b/src/app/features/institutions/models/institution-index-value-search-json-api.model.ts @@ -5,7 +5,7 @@ export interface InstitutionSearchResultCount { cardSearchResultCount: number; }; id: string; - type: 'search-result'; + type: string; relationships: { indexCard: { data: { @@ -22,10 +22,11 @@ export interface InstitutionIndexCardFilter { resourceMetadata: { displayLabel: { '@value': string }[]; '@id': string; + name: { '@value': string }[]; }; }; id: string; - type: 'index-card'; + type: string; } export type InstitutionIndexValueSearchIncludedJsonApi = InstitutionSearchResultCount | InstitutionIndexCardFilter; @@ -39,6 +40,6 @@ export interface InstitutionIndexValueSearchJsonApi export interface InstitutionSearchFilter { id: string; label: string; - value: string; + value: string | number; count?: number; } diff --git a/src/app/features/institutions/models/institution-summary-metric.model.ts b/src/app/features/institutions/models/institution-summary-metric.model.ts new file mode 100644 index 000000000..5e8af3486 --- /dev/null +++ b/src/app/features/institutions/models/institution-summary-metric.model.ts @@ -0,0 +1,13 @@ +export interface InstitutionSummaryMetrics { + reportYearmonth: string; + userCount: number; + publicProjectCount: number; + privateProjectCount: number; + publicRegistrationCount: number; + embargoedRegistrationCount: number; + publishedPreprintCount: number; + publicFileCount: number; + storageByteCount: number; + monthlyLoggedInUserCount: number; + monthlyActiveUserCount: number; +} diff --git a/src/app/features/institutions/pages/admin/admin-institutions.component.html b/src/app/features/institutions/pages/admin/admin-institutions.component.html new file mode 100644 index 000000000..9d9173309 --- /dev/null +++ b/src/app/features/institutions/pages/admin/admin-institutions.component.html @@ -0,0 +1,32 @@ +
+ @if (isInstitutionLoading()) { +
+ +
+ } @else { +
+
+ + +

{{ institution().name }}

+
+
+ } + + +
+ +
+
diff --git a/src/app/features/institutions/pages/admin/admin-institutions.component.scss b/src/app/features/institutions/pages/admin/admin-institutions.component.scss new file mode 100644 index 000000000..da0c027b5 --- /dev/null +++ b/src/app/features/institutions/pages/admin/admin-institutions.component.scss @@ -0,0 +1,5 @@ +:host { + display: flex; + flex-direction: column; + flex: 1; +} diff --git a/src/app/features/institutions/pages/admin/admin-institutions.component.spec.ts b/src/app/features/institutions/pages/admin/admin-institutions.component.spec.ts new file mode 100644 index 000000000..458f9580e --- /dev/null +++ b/src/app/features/institutions/pages/admin/admin-institutions.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminInstitutionsComponent } from './admin-institutions.component'; + +describe('AdminInstitutionsComponent', () => { + let component: AdminInstitutionsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AdminInstitutionsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(AdminInstitutionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/institutions/pages/admin/admin-institutions.component.ts b/src/app/features/institutions/pages/admin/admin-institutions.component.ts new file mode 100644 index 000000000..7bf7e6121 --- /dev/null +++ b/src/app/features/institutions/pages/admin/admin-institutions.component.ts @@ -0,0 +1,67 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslateModule } from '@ngx-translate/core'; + +import { TabsModule } from 'primeng/tabs'; + +import { NgOptimizedImage } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { ActivatedRoute, Router, RouterOutlet } from '@angular/router'; + +import { LoadingSpinnerComponent } from '@shared/components'; +import { FetchInstitutionById, InstitutionsSearchSelectors } from '@shared/stores'; + +interface TabOption { + label: string; + value: string; + route: string; +} + +@Component({ + selector: 'osf-admin-institutions', + imports: [TabsModule, TranslateModule, RouterOutlet, NgOptimizedImage, LoadingSpinnerComponent], + templateUrl: './admin-institutions.component.html', + styleUrl: './admin-institutions.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, +}) +export class AdminInstitutionsComponent implements OnInit { + private readonly router = inject(Router); + private readonly route = inject(ActivatedRoute); + + institution = select(InstitutionsSearchSelectors.getInstitution); + isInstitutionLoading = select(InstitutionsSearchSelectors.getInstitutionLoading); + + private readonly actions = createDispatchMap({ + fetchInstitution: FetchInstitutionById, + }); + + selectedTab = 'summary'; + + resourceTabOptions: TabOption[] = [ + { label: 'Summary', value: 'summary', route: 'summary' }, + { label: 'Users', value: 'users', route: 'users' }, + { label: 'Projects', value: 'projects', route: 'projects' }, + { label: 'Registrations', value: 'registrations', route: 'registrations' }, + { label: 'Preprints', value: 'preprints', route: 'preprints' }, + ]; + + ngOnInit() { + const institutionId = this.route.snapshot.params['institution-id']; + + if (institutionId) { + this.actions.fetchInstitution(institutionId); + } + + this.selectedTab = this.route.snapshot.firstChild?.routeConfig?.path || 'summary'; + } + + onTabChange(selectedValue: string | number) { + const value = String(selectedValue); + this.selectedTab = value; + const selectedTab = this.resourceTabOptions.find((tab) => tab.value === value); + if (selectedTab) { + this.router.navigate([selectedTab.route], { relativeTo: this.route }); + } + } +} diff --git a/src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.html b/src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.html new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.scss b/src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.spec.ts b/src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.spec.ts new file mode 100644 index 000000000..ff255377d --- /dev/null +++ b/src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InstitutionsPreprintsComponent } from './institutions-preprints.component'; + +describe('InstitutionsPreprintsComponent', () => { + let component: InstitutionsPreprintsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InstitutionsPreprintsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(InstitutionsPreprintsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.ts b/src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.ts new file mode 100644 index 000000000..493f5ce8b --- /dev/null +++ b/src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'osf-institutions-preprints', + imports: [], + templateUrl: './institutions-preprints.component.html', + styleUrl: './institutions-preprints.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class InstitutionsPreprintsComponent {} diff --git a/src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.html b/src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.html new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.scss b/src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.spec.ts b/src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.spec.ts new file mode 100644 index 000000000..082911061 --- /dev/null +++ b/src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InstitutionsProjectsComponent } from './institutions-projects.component'; + +describe('InstitutionsProjectsComponent', () => { + let component: InstitutionsProjectsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InstitutionsProjectsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(InstitutionsProjectsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.ts b/src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.ts new file mode 100644 index 000000000..066666964 --- /dev/null +++ b/src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'osf-institutions-projects', + imports: [], + templateUrl: './institutions-projects.component.html', + styleUrl: './institutions-projects.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class InstitutionsProjectsComponent {} diff --git a/src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.html b/src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.html new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.scss b/src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.spec.ts b/src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.spec.ts new file mode 100644 index 000000000..12db9ff06 --- /dev/null +++ b/src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InstitutionsRegistrationsComponent } from './institutions-registrations.component'; + +describe('InstitutionsRegistrationsComponent', () => { + let component: InstitutionsRegistrationsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InstitutionsRegistrationsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(InstitutionsRegistrationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.ts b/src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.ts new file mode 100644 index 000000000..5dbe432ab --- /dev/null +++ b/src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'osf-institutions-registrations', + imports: [], + templateUrl: './institutions-registrations.component.html', + styleUrl: './institutions-registrations.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class InstitutionsRegistrationsComponent {} diff --git a/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.html b/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.html index a463bb8d7..ec7848922 100644 --- a/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.html +++ b/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.html @@ -1 +1,89 @@ -

institutions-summary works!

+@if (summaryMetricsLoading()) { +
+ +
+} @else { +
+
+ @for (item of statisticsData; track $index) { + + } +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+} diff --git a/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.scss b/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.scss index e69de29bb..f865d569d 100644 --- a/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.scss +++ b/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.scss @@ -0,0 +1,15 @@ +.width-25 { + width: calc(25% - 1.5rem); + + @media (max-width: 576px) { + width: calc(50% - 0.5rem); + } +} + +.width-33 { + width: calc(33% - 1rem); + + @media (max-width: 576px) { + width: 100%; + } +} diff --git a/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.ts b/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.ts index 83838bc76..b39707283 100644 --- a/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.ts +++ b/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.ts @@ -1,62 +1,196 @@ import { createDispatchMap, select } from '@ngxs/store'; -import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; + +import { ChangeDetectionStrategy, Component, effect, inject, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { BarChartComponent, LoadingSpinnerComponent, StatisticCardComponent } from '@shared/components'; +import { DoughnutChartComponent } from '@shared/components/doughnut-chart/doughnut-chart.component'; +import { DatasetInput, SelectOption } from '@shared/models'; + import { FetchHasOsfAddonSearch, FetchInstitutionDepartments, + FetchInstitutionSearchResults, FetchInstitutionSummaryMetrics, FetchStorageRegionSearch, InstitutionsAdminSelectors, - SetSelectedInstitutionId, } from '../../../../store'; @Component({ selector: 'osf-institutions-summary', - imports: [], + imports: [StatisticCardComponent, LoadingSpinnerComponent, DoughnutChartComponent, BarChartComponent, TranslatePipe], templateUrl: './institutions-summary.component.html', styleUrl: './institutions-summary.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, }) export class InstitutionsSummaryComponent implements OnInit { private readonly route = inject(ActivatedRoute); + private readonly translateService = inject(TranslateService); + + statisticsData: SelectOption[] = []; departments = select(InstitutionsAdminSelectors.getDepartments); departmentsLoading = select(InstitutionsAdminSelectors.getDepartmentsLoading); - departmentsError = select(InstitutionsAdminSelectors.getDepartmentsError); summaryMetrics = select(InstitutionsAdminSelectors.getSummaryMetrics); summaryMetricsLoading = select(InstitutionsAdminSelectors.getSummaryMetricsLoading); - summaryMetricsError = select(InstitutionsAdminSelectors.getSummaryMetricsError); hasOsfAddonSearch = select(InstitutionsAdminSelectors.getHasOsfAddonSearch); hasOsfAddonSearchLoading = select(InstitutionsAdminSelectors.getHasOsfAddonSearchLoading); - hasOsfAddonSearchError = select(InstitutionsAdminSelectors.getHasOsfAddonSearchError); storageRegionSearch = select(InstitutionsAdminSelectors.getStorageRegionSearch); storageRegionSearchLoading = select(InstitutionsAdminSelectors.getStorageRegionSearchLoading); - storageRegionSearchError = select(InstitutionsAdminSelectors.getStorageRegionSearchError); - selectedInstitutionId = select(InstitutionsAdminSelectors.getSelectedInstitutionId); + rightsSearch = select(InstitutionsAdminSelectors.getSearchResults); + rightsLoading = select(InstitutionsAdminSelectors.getSearchResultsLoading); + + protected departmentLabels: string[] = []; + protected departmentDataset: DatasetInput[] = []; + + protected projectsLabels: string[] = []; + protected projectDataset: DatasetInput[] = []; + + protected registrationsLabels: string[] = []; + protected registrationsDataset: DatasetInput[] = []; + + protected osfProjectsLabels: string[] = []; + protected osfProjectsDataset: DatasetInput[] = []; + + protected storageLabels: string[] = []; + protected storageDataset: DatasetInput[] = []; + + protected licenceLabels: string[] = []; + protected licenceDataset: DatasetInput[] = []; + + protected addonLabels: string[] = []; + protected addonDataset: DatasetInput[] = []; private readonly actions = createDispatchMap({ fetchDepartments: FetchInstitutionDepartments, fetchSummaryMetrics: FetchInstitutionSummaryMetrics, fetchHasOsfAddonSearch: FetchHasOsfAddonSearch, fetchStorageRegionSearch: FetchStorageRegionSearch, - setSelectedInstitutionId: SetSelectedInstitutionId, + fetchSearchResults: FetchInstitutionSearchResults, }); + constructor() { + effect(() => { + this.setStatisticSummaryData(); + this.setChartData(); + }); + } + ngOnInit(): void { - const institutionId = this.route.snapshot.params['institution-id']; + const institutionId = this.route.parent?.snapshot.params['institution-id']; if (institutionId) { - this.actions.setSelectedInstitutionId(institutionId); + this.actions.fetchSearchResults(institutionId, 'rights'); this.actions.fetchDepartments(institutionId); this.actions.fetchSummaryMetrics(institutionId); this.actions.fetchHasOsfAddonSearch(institutionId); this.actions.fetchStorageRegionSearch(institutionId); } } + + private setStatisticSummaryData(): void { + const summary = this.summaryMetrics(); + + if (summary) { + this.statisticsData = [ + { + label: 'adminInstitutions.summary.totalUsers', + value: summary.userCount, + }, + { + label: 'adminInstitutions.summary.totalMonthlyLoggedInUsers', + value: summary.monthlyLoggedInUserCount, + }, + { + label: 'adminInstitutions.summary.totalMonthlyActiveUsers', + value: summary.monthlyActiveUserCount, + }, + { + label: 'adminInstitutions.summary.osfPublicAndPrivateProjects', + value: summary.publicProjectCount + summary.privateProjectCount, + }, + { + label: 'adminInstitutions.summary.osfPublicAndEmbargoedRegistrations', + value: summary.publicRegistrationCount + summary.embargoedRegistrationCount, + }, + { + label: 'adminInstitutions.summary.osfPreprints', + value: summary.publishedPreprintCount, + }, + { + label: 'adminInstitutions.summary.totalPublicFileCount', + value: summary.publicFileCount, + }, + { + label: 'adminInstitutions.summary.totalStorageInGb', + value: this.convertBytesToGB(summary.storageByteCount), + }, + ]; + } + } + + private setChartData(): void { + const departments = this.departments(); + const summary = this.summaryMetrics(); + const storage = this.storageRegionSearch(); + const licenses = this.rightsSearch(); + const addons = this.hasOsfAddonSearch(); + + this.departmentLabels = departments.data.map((item) => item.attributes.name || ''); + this.departmentDataset = [{ label: '', data: departments.data.map((item) => item.attributes.number_of_users) }]; + + this.projectsLabels = ['resourceCard.labels.publicProjects', 'adminInstitutions.summary.privateProjects'].map( + (el) => this.translateService.instant(el) + ); + this.projectDataset = [{ label: '', data: [summary.publicProjectCount, summary.privateProjectCount] }]; + + this.registrationsLabels = [ + 'resourceCard.labels.publicRegistrations', + 'adminInstitutions.summary.embargoedRegistrations', + ].map((el) => this.translateService.instant(el)); + this.registrationsDataset = [ + { label: '', data: [summary.publicRegistrationCount, summary.embargoedRegistrationCount] }, + ]; + + this.osfProjectsLabels = [ + 'resourceCard.labels.publicRegistrations', + 'adminInstitutions.summary.embargoedRegistrations', + 'resourceCard.labels.publicProjects', + 'adminInstitutions.summary.privateProjects', + 'common.search.tabs.preprints', + ].map((el) => this.translateService.instant(el)); + this.osfProjectsDataset = [ + { + label: '', + data: [ + summary.publicRegistrationCount, + summary.embargoedRegistrationCount, + summary.publicProjectCount, + summary.privateProjectCount, + summary.publishedPreprintCount, + ], + }, + ]; + + this.storageLabels = storage.map((result) => result.label); + this.storageDataset = [{ label: '', data: storage.map((result) => +result.value) }]; + + this.licenceLabels = licenses.map((result) => result.label); + this.licenceDataset = [{ label: '', data: licenses.map((result) => +result.value) }]; + + this.addonLabels = addons.map((result) => result.label); + this.addonDataset = [{ label: '', data: addons.map((result) => +result.value) }]; + } + + private convertBytesToGB(bytes: number): string { + const gb = bytes / (1024 * 1024 * 1024); + return gb.toFixed(1); + } } diff --git a/src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.html b/src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.html new file mode 100644 index 000000000..d6165891c --- /dev/null +++ b/src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.html @@ -0,0 +1,4 @@ +
+

Institution Users

+

Users content will be implemented here...

+
diff --git a/src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.scss b/src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.spec.ts b/src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.spec.ts new file mode 100644 index 000000000..f36d30cc3 --- /dev/null +++ b/src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InstitutionsUsersComponent } from './institutions-users.component'; + +describe('InstitutionsUsersComponent', () => { + let component: InstitutionsUsersComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InstitutionsUsersComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(InstitutionsUsersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.ts b/src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.ts new file mode 100644 index 000000000..ba4465712 --- /dev/null +++ b/src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'osf-institutions-users', + imports: [], + templateUrl: './institutions-users.component.html', + styleUrl: './institutions-users.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class InstitutionsUsersComponent {} diff --git a/src/app/features/institutions/pages/admin/pages/routes.ts b/src/app/features/institutions/pages/admin/pages/routes.ts deleted file mode 100644 index 99aa60117..000000000 --- a/src/app/features/institutions/pages/admin/pages/routes.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { provideStates } from '@ngxs/store'; - -import { Routes } from '@angular/router'; - -import { InstitutionsSummaryComponent } from '@osf/features/institutions/pages/admin/pages'; -import { InstitutionsAdminState } from '@osf/features/institutions/store'; - -export const routes: Routes = [ - { - path: '', - component: InstitutionsSummaryComponent, - providers: [provideStates([InstitutionsAdminState])], - }, -]; diff --git a/src/app/features/institutions/pages/admin/routes.ts b/src/app/features/institutions/pages/admin/routes.ts new file mode 100644 index 000000000..ec21441ba --- /dev/null +++ b/src/app/features/institutions/pages/admin/routes.ts @@ -0,0 +1,46 @@ +import { provideStates } from '@ngxs/store'; + +import { Routes } from '@angular/router'; + +import { AdminInstitutionsComponent } from '@osf/features/institutions/pages/admin/admin-institutions.component'; +import { InstitutionsPreprintsComponent } from '@osf/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component'; +import { InstitutionsProjectsComponent } from '@osf/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component'; +import { InstitutionsRegistrationsComponent } from '@osf/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component'; +import { InstitutionsSummaryComponent } from '@osf/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component'; +import { InstitutionsUsersComponent } from '@osf/features/institutions/pages/admin/pages/institutions-users/institutions-users.component'; +import { InstitutionsAdminState } from '@osf/features/institutions/store'; + +export const routes: Routes = [ + { + path: '', + component: AdminInstitutionsComponent, + providers: [provideStates([InstitutionsAdminState])], + children: [ + { + path: '', + redirectTo: 'summary', + pathMatch: 'full', + }, + { + path: 'summary', + component: InstitutionsSummaryComponent, + }, + { + path: 'users', + component: InstitutionsUsersComponent, + }, + { + path: 'projects', + component: InstitutionsProjectsComponent, + }, + { + path: 'registrations', + component: InstitutionsRegistrationsComponent, + }, + { + path: 'preprints', + component: InstitutionsPreprintsComponent, + }, + ], + }, +]; diff --git a/src/app/features/institutions/services/institutions-admin.service.ts b/src/app/features/institutions/services/institutions-admin.service.ts index e9b978aa8..0637167db 100644 --- a/src/app/features/institutions/services/institutions-admin.service.ts +++ b/src/app/features/institutions/services/institutions-admin.service.ts @@ -1,14 +1,16 @@ -import { map, Observable } from 'rxjs'; +import { catchError, map, Observable, of } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { JsonApiService } from '@osf/core/services'; +import { mapInstitutionSummaryMetrics } from '@osf/features/institutions/mappers'; +import { mapIndexCardResults } from '@osf/features/institutions/mappers/institution-summary-index.mapper'; import { InstitutionDepartmentsJsonApi, - InstitutionIndexCardFilter, InstitutionIndexValueSearchJsonApi, InstitutionSearchFilter, + InstitutionSummaryMetrics, InstitutionSummaryMetricsJsonApi, } from '../models'; @@ -18,18 +20,118 @@ import { environment } from 'src/environments/environment'; providedIn: 'root', }) export class InstitutionsAdminService { + hardcodedUrl = 'https://api.test.osf.io/v2'; + private jsonApiService = inject(JsonApiService); fetchDepartments(institutionId: string): Observable { - return this.jsonApiService.get( - `${environment.apiUrl}/institutions/${institutionId}/metrics/departments/` - ); + return this.jsonApiService + .get(`${this.hardcodedUrl}/institutions/${institutionId}/metrics/departments/`) + .pipe( + catchError((err) => { + console.warn('Departments API error, returning mock data:', err); + return of({ + data: [ + { + id: 'cos-N/A', + type: 'institution-departments', + attributes: { + name: 'N/A', + number_of_users: 159, + }, + links: { + self: 'https://api.test.osf.io/v2/institutions/cos/metrics/departments/', + }, + }, + { + id: 'cos-', + type: 'institution-departments', + attributes: { + name: '', + number_of_users: 9, + }, + links: { + self: 'https://api.test.osf.io/v2/institutions/cos/metrics/departments/', + }, + }, + { + id: 'cos-QA', + type: 'institution-departments', + attributes: { + name: 'QA', + number_of_users: 5, + }, + links: { + self: 'https://api.test.osf.io/v2/institutions/cos/metrics/departments/', + }, + }, + ], + meta: { + total: 3, + per_page: 10, + version: '2.20', + }, + links: { + self: 'https://api.test.osf.io/v2/institutions/cos/metrics/departments/', + first: null, + last: null, + prev: null, + next: null, + }, + } as InstitutionDepartmentsJsonApi); + }) + ); } - fetchSummary(institutionId: string): Observable { - return this.jsonApiService.get( - `${environment.apiUrl}/institutions/${institutionId}/metrics/summary/` - ); + fetchSummary(institutionId: string): Observable { + return this.jsonApiService + .get(`${this.hardcodedUrl}/institutions/${institutionId}/metrics/summary/`) + .pipe( + catchError((err) => { + console.warn('Summary API error, returning mock data:', err); + return of({ + data: { + id: 'cos', + type: 'institution-summary-metrics', + attributes: { + report_yearmonth: '2025-06', + user_count: 173, + public_project_count: 174, + private_project_count: 839, + public_registration_count: 360, + embargoed_registration_count: 76, + published_preprint_count: 1464, + public_file_count: 7088, + storage_byte_count: 12365253682, + monthly_logged_in_user_count: 14, + monthly_active_user_count: 15, + }, + relationships: { + user: { + data: null, + }, + institution: { + links: { + related: { + href: 'https://api.test.osf.io/v2/institutions/cos/', + meta: {}, + }, + }, + data: { + id: 'cos', + type: 'institutions', + }, + }, + }, + links: {}, + }, + meta: { + version: '2.20', + }, + } as InstitutionSummaryMetricsJsonApi); + }), + map((result) => mapInstitutionSummaryMetrics(result.data.attributes)) + ); } fetchIndexValueSearch( @@ -46,19 +148,6 @@ export class InstitutionsAdminService { return this.jsonApiService .get(`${environment.shareDomainUrl}/index-value-search`, params) - .pipe( - map((response) => { - if (response?.included) { - return response.included - .filter((item): item is InstitutionIndexCardFilter => item.type === 'index-card') - .map((item) => ({ - id: item.id, - label: item.attributes?.resourceMetadata?.displayLabel?.[0]?.['@value'] || item.id, - value: item.attributes?.resourceMetadata?.['@id'] || item.id, - })); - } - return []; - }) - ); + .pipe(map((response) => mapIndexCardResults(response?.included))); } } diff --git a/src/app/features/institutions/store/institutions-admin.model.ts b/src/app/features/institutions/store/institutions-admin.model.ts index 0d8d14fd2..1e2488931 100644 --- a/src/app/features/institutions/store/institutions-admin.model.ts +++ b/src/app/features/institutions/store/institutions-admin.model.ts @@ -1,10 +1,10 @@ import { AsyncStateModel } from '@shared/models'; -import { InstitutionDepartmentsJsonApi, InstitutionSearchFilter, InstitutionSummaryMetricsJsonApi } from '../models'; +import { InstitutionDepartmentsJsonApi, InstitutionSearchFilter, InstitutionSummaryMetrics } from '../models'; export interface InstitutionsAdminModel { departments: AsyncStateModel; - summaryMetrics: AsyncStateModel; + summaryMetrics: AsyncStateModel; hasOsfAddonSearch: AsyncStateModel; storageRegionSearch: AsyncStateModel; searchResults: AsyncStateModel; diff --git a/src/app/features/institutions/store/institutions-admin.selectors.ts b/src/app/features/institutions/store/institutions-admin.selectors.ts index 359f36a6e..12212411d 100644 --- a/src/app/features/institutions/store/institutions-admin.selectors.ts +++ b/src/app/features/institutions/store/institutions-admin.selectors.ts @@ -1,6 +1,6 @@ import { Selector } from '@ngxs/store'; -import { InstitutionDepartmentsJsonApi, InstitutionSearchFilter, InstitutionSummaryMetricsJsonApi } from '../models'; +import { InstitutionDepartmentsJsonApi, InstitutionSearchFilter, InstitutionSummaryMetrics } from '../models'; import { InstitutionsAdminModel } from './institutions-admin.model'; import { InstitutionsAdminState } from './institutions-admin.state'; @@ -22,7 +22,7 @@ export class InstitutionsAdminSelectors { } @Selector([InstitutionsAdminState]) - static getSummaryMetrics(state: InstitutionsAdminModel): InstitutionSummaryMetricsJsonApi { + static getSummaryMetrics(state: InstitutionsAdminModel): InstitutionSummaryMetrics { return state.summaryMetrics.data; } diff --git a/src/app/features/institutions/store/institutions-admin.state.ts b/src/app/features/institutions/store/institutions-admin.state.ts index 178c8cfc5..336440b08 100644 --- a/src/app/features/institutions/store/institutions-admin.state.ts +++ b/src/app/features/institutions/store/institutions-admin.state.ts @@ -4,17 +4,20 @@ import { catchError, tap, throwError } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { InstitutionDepartmentsJsonApi, InstitutionSearchFilter, InstitutionSummaryMetricsJsonApi } from '../models'; +import { + InstitutionDepartmentsJsonApi, + InstitutionSearchFilter, + InstitutionSummaryMetrics, + InstitutionSummaryMetricsJsonApi, +} from '../models'; import { InstitutionsAdminService } from '../services/institutions-admin.service'; import { - ClearInstitutionsAdminData, FetchHasOsfAddonSearch, FetchInstitutionDepartments, FetchInstitutionSearchResults, FetchInstitutionSummaryMetrics, FetchStorageRegionSearch, - SetSelectedInstitutionId, } from './institutions-admin.actions'; import { InstitutionsAdminModel } from './institutions-admin.model'; @@ -69,7 +72,7 @@ const createEmptySearchFilters = (): InstitutionSearchFilter[] => []; name: 'institutionsAdmin', defaults: { departments: { data: createEmptyDepartments(), isLoading: false, error: null }, - summaryMetrics: { data: createEmptySummaryMetrics(), isLoading: false, error: null }, + summaryMetrics: { data: {} as InstitutionSummaryMetrics, isLoading: false, error: null }, hasOsfAddonSearch: { data: createEmptySearchFilters(), isLoading: false, error: null }, storageRegionSearch: { data: createEmptySearchFilters(), isLoading: false, error: null }, searchResults: { data: createEmptySearchFilters(), isLoading: false, error: null }, @@ -99,7 +102,7 @@ export class InstitutionsAdminState { departments: { ...state.departments, isLoading: false, - error: error.message || 'Failed to fetch departments', + error: error.message, }, }); return throwError(() => error); @@ -125,7 +128,7 @@ export class InstitutionsAdminState { summaryMetrics: { ...state.summaryMetrics, isLoading: false, - error: error.message || 'Failed to fetch summary metrics', + error: error.message, }, }); return throwError(() => error); @@ -154,7 +157,7 @@ export class InstitutionsAdminState { searchResults: { ...state.searchResults, isLoading: false, - error: error.message || 'Failed to fetch search results', + error: error.message, }, }); return throwError(() => error); @@ -180,7 +183,7 @@ export class InstitutionsAdminState { hasOsfAddonSearch: { ...state.hasOsfAddonSearch, isLoading: false, - error: error.message || 'Failed to fetch has OSF addon search', + error: error.message, }, }); return throwError(() => error); @@ -206,31 +209,11 @@ export class InstitutionsAdminState { storageRegionSearch: { ...state.storageRegionSearch, isLoading: false, - error: error.message || 'Failed to fetch storage region search', + error: error.message, }, }); return throwError(() => error); }) ); } - - @Action(SetSelectedInstitutionId) - setSelectedInstitutionId(ctx: StateContext, action: SetSelectedInstitutionId) { - ctx.patchState({ - selectedInstitutionId: action.institutionId, - }); - } - - @Action(ClearInstitutionsAdminData) - clearData(ctx: StateContext) { - ctx.patchState({ - departments: { data: createEmptyDepartments(), isLoading: false, error: null }, - summaryMetrics: { data: createEmptySummaryMetrics(), isLoading: false, error: null }, - hasOsfAddonSearch: { data: createEmptySearchFilters(), isLoading: false, error: null }, - storageRegionSearch: { data: createEmptySearchFilters(), isLoading: false, error: null }, - searchResults: { data: createEmptySearchFilters(), isLoading: false, error: null }, - selectedInstitutionId: null, - currentSearchPropertyPath: null, - }); - } } diff --git a/src/app/shared/components/bar-chart/bar-chart.component.html b/src/app/shared/components/bar-chart/bar-chart.component.html index 2dcd65140..11d299c7a 100644 --- a/src/app/shared/components/bar-chart/bar-chart.component.html +++ b/src/app/shared/components/bar-chart/bar-chart.component.html @@ -1,9 +1,35 @@ -

{{ title() | translate }}

+@if (!showExpandedSection()) { +

{{ title() | translate }}

+} @if (isLoading()) { } @else { -
- +
+
+ +
+ + @if (showExpandedSection()) { +
+ + + +

{{ title() | translate }}

+
+ +
+ @for (label of labels(); let i = $index; track i) { +
  • +
    + {{ label }} +
  • + } +
    +
    +
    +
    +
    + }
    } diff --git a/src/app/shared/components/bar-chart/bar-chart.component.ts b/src/app/shared/components/bar-chart/bar-chart.component.ts index 898c5da11..63ed05c0e 100644 --- a/src/app/shared/components/bar-chart/bar-chart.component.ts +++ b/src/app/shared/components/bar-chart/bar-chart.component.ts @@ -1,5 +1,6 @@ import { TranslatePipe } from '@ngx-translate/core'; +import { Accordion, AccordionContent, AccordionHeader, AccordionPanel } from 'primeng/accordion'; import { ChartModule } from 'primeng/chart'; import { isPlatformBrowser } from '@angular/common'; @@ -15,6 +16,7 @@ import { } from '@angular/core'; import { DatasetInput } from '@osf/shared/models'; +import { PIE_CHART_PALETTE } from '@shared/utils'; import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.component'; @@ -22,10 +24,19 @@ import { ChartData, ChartOptions } from 'chart.js'; @Component({ selector: 'osf-bar-chart', - imports: [ChartModule, TranslatePipe, LoadingSpinnerComponent], + imports: [ + ChartModule, + TranslatePipe, + LoadingSpinnerComponent, + AccordionContent, + AccordionHeader, + AccordionPanel, + Accordion, + ], templateUrl: './bar-chart.component.html', styleUrl: './bar-chart.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, }) export class BarChartComponent implements OnInit { isLoading = input(false); @@ -34,7 +45,10 @@ export class BarChartComponent implements OnInit { datasets = input([]); showLegend = input(false); showGrid = input(false); + showTicks = input(true); + orientation = input<'horizontal' | 'vertical'>('horizontal'); + showExpandedSection = input(false); protected options = signal({}); protected data = signal({} as ChartData); @@ -75,6 +89,10 @@ export class BarChartComponent implements OnInit { }); } + getColor(index: number): string { + return PIE_CHART_PALETTE[index % PIE_CHART_PALETTE.length]; + } + private setChartOptions(textColorSecondary: string, surfaceBorder: string) { this.options.set({ indexAxis: this.orientation() === 'horizontal' ? 'y' : 'x', @@ -89,6 +107,7 @@ export class BarChartComponent implements OnInit { scales: { x: { ticks: { + display: this.showTicks(), color: textColorSecondary, }, grid: { @@ -98,6 +117,7 @@ export class BarChartComponent implements OnInit { }, y: { ticks: { + display: this.showTicks(), color: textColorSecondary, }, grid: { diff --git a/src/app/shared/components/doughnut-chart/doughnut-chart.component.html b/src/app/shared/components/doughnut-chart/doughnut-chart.component.html index efa813004..a10489f3e 100644 --- a/src/app/shared/components/doughnut-chart/doughnut-chart.component.html +++ b/src/app/shared/components/doughnut-chart/doughnut-chart.component.html @@ -1,9 +1,35 @@ -

    {{ title() | translate }}

    +@if (!showExpandedSection()) { +

    {{ title() | translate }}

    +} @if (isLoading()) { } @else { -
    - +
    +
    + +
    + + @if (showExpandedSection()) { +
    + + + +

    {{ title() | translate }}

    +
    + +
    + @for (label of labels(); let i = $index; track i) { +
  • +
    + {{ label }} +
  • + } +
    +
    +
    +
    +
    + }
    } diff --git a/src/app/shared/components/doughnut-chart/doughnut-chart.component.scss b/src/app/shared/components/doughnut-chart/doughnut-chart.component.scss index 2061ecc7a..26cf0932b 100644 --- a/src/app/shared/components/doughnut-chart/doughnut-chart.component.scss +++ b/src/app/shared/components/doughnut-chart/doughnut-chart.component.scss @@ -1,5 +1,6 @@ :host { display: block; + height: 100%; } .chart-title { diff --git a/src/app/shared/components/doughnut-chart/doughnut-chart.component.ts b/src/app/shared/components/doughnut-chart/doughnut-chart.component.ts index e185ba496..5d647dd51 100644 --- a/src/app/shared/components/doughnut-chart/doughnut-chart.component.ts +++ b/src/app/shared/components/doughnut-chart/doughnut-chart.component.ts @@ -1,5 +1,6 @@ import { TranslatePipe } from '@ngx-translate/core'; +import { Accordion, AccordionContent, AccordionHeader, AccordionPanel } from 'primeng/accordion'; import { ChartModule } from 'primeng/chart'; import { isPlatformBrowser } from '@angular/common'; @@ -22,43 +23,57 @@ import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.comp import { ChartData, ChartOptions } from 'chart.js'; @Component({ - selector: 'osf-pie-chart', - imports: [ChartModule, TranslatePipe, LoadingSpinnerComponent], + selector: 'osf-doughnut-chart', + imports: [ + ChartModule, + TranslatePipe, + LoadingSpinnerComponent, + Accordion, + AccordionHeader, + AccordionPanel, + AccordionContent, + ], templateUrl: './doughnut-chart.component.html', styleUrl: './doughnut-chart.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, }) export class DoughnutChartComponent implements OnInit { + private readonly platformId = inject(PLATFORM_ID); + private readonly cd = inject(ChangeDetectorRef); + isLoading = input(false); title = input(''); labels = input([]); datasets = input([]); showLegend = input(false); + showExpandedSection = input(false); protected options = signal({}); protected data = signal({} as ChartData); - #platformId = inject(PLATFORM_ID); - #cd = inject(ChangeDetectorRef); - ngOnInit() { this.initChart(); } initChart() { - if (isPlatformBrowser(this.#platformId)) { + if (isPlatformBrowser(this.platformId)) { this.setChartData(); this.setChartOptions(); - this.#cd.markForCheck(); + this.cd.markForCheck(); } } + getColor(index: number): string { + return PIE_CHART_PALETTE[index % PIE_CHART_PALETTE.length]; + } + private setChartData() { const chartDatasets = this.datasets().map((dataset) => ({ label: dataset.label, data: dataset.data, - backgroundColor: PIE_CHART_PALETTE, + backgroundColor: dataset?.color || PIE_CHART_PALETTE, borderWidth: 0, })); diff --git a/src/app/shared/components/index.ts b/src/app/shared/components/index.ts index 80d1049bc..762c98247 100644 --- a/src/app/shared/components/index.ts +++ b/src/app/shared/components/index.ts @@ -28,6 +28,7 @@ export { SearchHelpTutorialComponent } from './search-help-tutorial/search-help- export { SearchInputComponent } from './search-input/search-input.component'; export { SearchResultsContainerComponent } from './search-results-container/search-results-container.component'; export { SelectComponent } from './select/select.component'; +export { StatisticCardComponent } from './statistic-card/statistic-card.component'; export { StepperComponent } from './stepper/stepper.component'; export { SubHeaderComponent } from './sub-header/sub-header.component'; export { SubjectsComponent } from './subjects/subjects.component'; diff --git a/src/app/shared/components/statistic-card/statistic-card.component.html b/src/app/shared/components/statistic-card/statistic-card.component.html new file mode 100644 index 000000000..00094df87 --- /dev/null +++ b/src/app/shared/components/statistic-card/statistic-card.component.html @@ -0,0 +1,6 @@ +
    +
    +

    {{ value() }}

    +
    +

    {{ label() }}

    +
    diff --git a/src/app/shared/components/statistic-card/statistic-card.component.scss b/src/app/shared/components/statistic-card/statistic-card.component.scss new file mode 100644 index 000000000..11732eb75 --- /dev/null +++ b/src/app/shared/components/statistic-card/statistic-card.component.scss @@ -0,0 +1,7 @@ +.bg-blue-2 { + background-color: var(--blue-2-bg); +} + +.col-blue-2 { + color: var(--blue-2); +} diff --git a/src/app/shared/components/statistic-card/statistic-card.component.spec.ts b/src/app/shared/components/statistic-card/statistic-card.component.spec.ts new file mode 100644 index 000000000..ff942c848 --- /dev/null +++ b/src/app/shared/components/statistic-card/statistic-card.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StatisticCardComponent } from './statistic-card.component'; + +describe('StatisticCardComponent', () => { + let component: StatisticCardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StatisticCardComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(StatisticCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/statistic-card/statistic-card.component.ts b/src/app/shared/components/statistic-card/statistic-card.component.ts new file mode 100644 index 000000000..c13f7ab3c --- /dev/null +++ b/src/app/shared/components/statistic-card/statistic-card.component.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import { Primitive } from '@core/helpers'; + +@Component({ + selector: 'osf-statistic-card', + templateUrl: './statistic-card.component.html', + styleUrl: './statistic-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, +}) +export class StatisticCardComponent { + value = input(); + label = input(''); +} diff --git a/src/app/shared/stores/institutions-search/institutions-search.state.ts b/src/app/shared/stores/institutions-search/institutions-search.state.ts index d20693065..84f0e5bab 100644 --- a/src/app/shared/stores/institutions-search/institutions-search.state.ts +++ b/src/app/shared/stores/institutions-search/institutions-search.state.ts @@ -1,4 +1,4 @@ -import { Action, NgxsOnInit, State, StateContext, Store } from '@ngxs/store'; +import { Action, NgxsOnInit, State, StateContext } from '@ngxs/store'; import { patch } from '@ngxs/store/operators'; import { BehaviorSubject, catchError, EMPTY, forkJoin, of, switchMap, tap, throwError } from 'rxjs'; @@ -46,7 +46,6 @@ import { InstitutionsSearchModel } from './institutions-search.model'; export class InstitutionsSearchState implements NgxsOnInit { private readonly institutionsService = inject(InstitutionsService); private readonly searchService = inject(SearchService); - private readonly store = inject(Store); private loadRequests = new BehaviorSubject<{ type: GetResourcesRequestTypeEnum; link?: string } | null>(null); private filterOptionsRequests = new BehaviorSubject(null); diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index c56fade29..22b709b2d 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1766,5 +1766,26 @@ "title": "Institutions", "description": "OSF Institutions enhances transparency and increases the visibility of research outputs, accelerating discovery and reuse. Institutional members focus on generating and sharing research, and let OSF Institutions handle the infrastructure.
    Read more", "searchInstitutions": "Search institutions" + }, + "adminInstitutions": { + "summary": { + "totalUsersByDepartment": "Total Users by Department", + "publicPrivateProjects": "Public vs Private Projects", + "publicEmbargoedRegistrations": "Public vs Embargoed Registrations", + "totalOsfObjects": "Total OSF Objects", + "topTenLicenses": "Top 10 Licenses", + "topTenAddons": "Top 10 Add-ons", + "topStorageRegions": "Top Storage Regions", + "totalUsers": "Total Users", + "totalMonthlyLoggedInUsers": "Total Monthly Logged in Users", + "totalMonthlyActiveUsers": "Total Monthly Active Users", + "osfPublicAndPrivateProjects": "OSF Public and Private Projects", + "osfPublicAndEmbargoedRegistrations": "OSF Public and Embargoed Registrations", + "osfPreprints": "OSF Preprints", + "totalPublicFileCount": "Total Public File Count", + "totalStorageInGb": "Total Storage in GB", + "embargoedRegistrations": "Embargoed registrations", + "privateProjects": "Private projects" + } } } From 99ee5b4d35bd40ed051505a0fadf28625b487c70 Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Mon, 7 Jul 2025 13:34:17 +0300 Subject: [PATCH 3/5] feat(isntitutions-summary): added summary page and admin structure --- .../search-results-container.component.html | 32 +++++++------------ .../search-results-container.component.ts | 12 +------ 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/src/app/shared/components/search-results-container/search-results-container.component.html b/src/app/shared/components/search-results-container/search-results-container.component.html index 7e98a712a..347523cbf 100644 --- a/src/app/shared/components/search-results-container/search-results-container.component.html +++ b/src/app/shared/components/search-results-container/search-results-container.component.html @@ -33,29 +33,21 @@

    > @if (isAnyFilterOptions()) { - filter by + > } - sort by + >

    diff --git a/src/app/shared/components/search-results-container/search-results-container.component.ts b/src/app/shared/components/search-results-container/search-results-container.component.ts index 070ae6d3f..b4fd1b5b6 100644 --- a/src/app/shared/components/search-results-container/search-results-container.component.ts +++ b/src/app/shared/components/search-results-container/search-results-container.component.ts @@ -4,7 +4,6 @@ import { Button } from 'primeng/button'; import { DataView } from 'primeng/dataview'; import { Select } from 'primeng/select'; -import { NgOptimizedImage } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core'; import { FormsModule } from '@angular/forms'; @@ -18,16 +17,7 @@ import { SelectComponent } from '../select/select.component'; @Component({ selector: 'osf-search-results-container', - imports: [ - FormsModule, - NgOptimizedImage, - Button, - DataView, - Select, - ResourceCardComponent, - TranslatePipe, - SelectComponent, - ], + imports: [FormsModule, Button, DataView, Select, ResourceCardComponent, TranslatePipe, SelectComponent], templateUrl: './search-results-container.component.html', styleUrl: './search-results-container.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, From 9fcc9ff068c425eef8df1b554d11033c0fd29a15 Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Mon, 7 Jul 2025 13:42:07 +0300 Subject: [PATCH 4/5] feat(isntitutions-summary): added summary page and admin structure --- .../institutions/pages/admin/admin-institutions.component.ts | 1 - .../institutions-summary/institutions-summary.component.ts | 1 - .../institutions/services/institutions-admin.service.ts | 3 +++ src/app/shared/components/bar-chart/bar-chart.component.ts | 1 - .../components/doughnut-chart/doughnut-chart.component.ts | 1 - .../components/statistic-card/statistic-card.component.ts | 1 - 6 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app/features/institutions/pages/admin/admin-institutions.component.ts b/src/app/features/institutions/pages/admin/admin-institutions.component.ts index 7bf7e6121..a8bc34b32 100644 --- a/src/app/features/institutions/pages/admin/admin-institutions.component.ts +++ b/src/app/features/institutions/pages/admin/admin-institutions.component.ts @@ -23,7 +23,6 @@ interface TabOption { templateUrl: './admin-institutions.component.html', styleUrl: './admin-institutions.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, }) export class AdminInstitutionsComponent implements OnInit { private readonly router = inject(Router); diff --git a/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.ts b/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.ts index b39707283..a5fb01cf7 100644 --- a/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.ts +++ b/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.ts @@ -24,7 +24,6 @@ import { templateUrl: './institutions-summary.component.html', styleUrl: './institutions-summary.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, }) export class InstitutionsSummaryComponent implements OnInit { private readonly route = inject(ActivatedRoute); diff --git a/src/app/features/institutions/services/institutions-admin.service.ts b/src/app/features/institutions/services/institutions-admin.service.ts index 0637167db..745a9e34b 100644 --- a/src/app/features/institutions/services/institutions-admin.service.ts +++ b/src/app/features/institutions/services/institutions-admin.service.ts @@ -28,6 +28,7 @@ export class InstitutionsAdminService { return this.jsonApiService .get(`${this.hardcodedUrl}/institutions/${institutionId}/metrics/departments/`) .pipe( + //todo: remove mock data catchError((err) => { console.warn('Departments API error, returning mock data:', err); return of({ @@ -87,6 +88,7 @@ export class InstitutionsAdminService { return this.jsonApiService .get(`${this.hardcodedUrl}/institutions/${institutionId}/metrics/summary/`) .pipe( + //todo: remove mock data catchError((err) => { console.warn('Summary API error, returning mock data:', err); return of({ @@ -140,6 +142,7 @@ export class InstitutionsAdminService { additionalParams?: Record ): Observable { const params: Record = { + //todo: change here https://test.osf.io to current environment 'cardSearchFilter[affiliation]': `https://ror.org/05d5mza29,https://test.osf.io/institutions/${institutionId}/`, valueSearchPropertyPath, 'page[size]': '10', diff --git a/src/app/shared/components/bar-chart/bar-chart.component.ts b/src/app/shared/components/bar-chart/bar-chart.component.ts index 63ed05c0e..59287845f 100644 --- a/src/app/shared/components/bar-chart/bar-chart.component.ts +++ b/src/app/shared/components/bar-chart/bar-chart.component.ts @@ -36,7 +36,6 @@ import { ChartData, ChartOptions } from 'chart.js'; templateUrl: './bar-chart.component.html', styleUrl: './bar-chart.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, }) export class BarChartComponent implements OnInit { isLoading = input(false); diff --git a/src/app/shared/components/doughnut-chart/doughnut-chart.component.ts b/src/app/shared/components/doughnut-chart/doughnut-chart.component.ts index 5d647dd51..99e115110 100644 --- a/src/app/shared/components/doughnut-chart/doughnut-chart.component.ts +++ b/src/app/shared/components/doughnut-chart/doughnut-chart.component.ts @@ -36,7 +36,6 @@ import { ChartData, ChartOptions } from 'chart.js'; templateUrl: './doughnut-chart.component.html', styleUrl: './doughnut-chart.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, }) export class DoughnutChartComponent implements OnInit { private readonly platformId = inject(PLATFORM_ID); diff --git a/src/app/shared/components/statistic-card/statistic-card.component.ts b/src/app/shared/components/statistic-card/statistic-card.component.ts index c13f7ab3c..fdca9ab6e 100644 --- a/src/app/shared/components/statistic-card/statistic-card.component.ts +++ b/src/app/shared/components/statistic-card/statistic-card.component.ts @@ -7,7 +7,6 @@ import { Primitive } from '@core/helpers'; templateUrl: './statistic-card.component.html', styleUrl: './statistic-card.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, }) export class StatisticCardComponent { value = input(); From e78493bff975e83f3ed0669f9066127176991cff Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Tue, 8 Jul 2025 08:33:37 +0300 Subject: [PATCH 5/5] feat(summary): added fixes by suggestions --- .../admin-institutions.component.html | 25 +-- .../admin-institutions.component.scss | 0 .../admin-institutions.component.spec.ts | 0 .../admin-institutions.component.ts | 17 +- .../constants/index.ts} | 0 .../constants/resource-tab-option.constant.ts | 9 + .../admin-institutions/mappers/index.ts | 2 + .../mappers/institution-departments.mapper.ts | 23 +++ .../institution-summary-index.mapper.ts | 14 +- .../institution-summary-metrics.mapper.ts | 7 +- .../models/index.ts | 2 + .../models/institution-department.model.ts | 6 + .../institution-departments-json-api.model.ts | 19 +++ ...ution-index-value-search-json-api.model.ts | 17 +- .../models/institution-search-filter.model.ts | 6 + .../institution-summary-metric.model.ts | 0 ...titution-summary-metrics-json-api.model.ts | 12 +- .../admin-institutions/pages/index.ts | 5 + .../institutions-preprints.component.html} | 0 .../institutions-preprints.component.scss} | 0 .../institutions-preprints.component.spec.ts | 0 .../institutions-preprints.component.ts | 0 .../institutions-projects.component.html} | 0 .../institutions-projects.component.scss} | 0 .../institutions-projects.component.spec.ts | 0 .../institutions-projects.component.ts | 0 ...institutions-registrations.component.html} | 0 ...institutions-registrations.component.scss} | 0 ...stitutions-registrations.component.spec.ts | 0 .../institutions-registrations.component.ts | 0 .../institutions-summary.component.html | 2 +- .../institutions-summary.component.scss | 0 .../institutions-summary.component.spec.ts | 0 .../institutions-summary.component.ts | 6 +- .../institutions-users.component.html | 0 .../institutions-users.component.scss | 0 .../institutions-users.component.spec.ts | 0 .../institutions-users.component.ts | 0 src/app/features/admin-institutions/routes.ts | 50 ++++++ .../services/index.ts | 0 .../services/institutions-admin.service.ts | 70 ++++++++ .../admin-institutions/services/mock.ts | 90 ++++++++++ .../store/index.ts | 0 .../store/institutions-admin.actions.ts | 0 .../store/institutions-admin.model.ts | 4 +- .../store/institutions-admin.selectors.ts | 4 +- .../store/institutions-admin.state.ts | 62 +------ .../institutions/institutions.component.html | 49 +----- .../institutions/institutions.component.ts | 132 +-------------- .../institutions/institutions.routes.ts | 27 +-- .../features/institutions/mappers/index.ts | 1 - .../institution-departments-json-api.model.ts | 19 --- .../institutions/pages/admin/pages/index.ts | 1 - .../institutions/pages/admin/routes.ts | 46 ------ src/app/features/institutions/pages/index.ts | 1 + .../institutions-list.component.html | 48 ++++++ .../institutions-list.component.spec.ts | 22 +++ .../institutions-list.component.ts | 134 +++++++++++++++ .../services/institutions-admin.service.ts | 156 ------------------ .../bar-chart/bar-chart.component.html | 4 +- .../bar-chart/bar-chart.component.scss | 11 -- .../doughnut-chart.component.html | 4 +- .../doughnut-chart.component.scss | 9 - .../line-chart/line-chart.component.html | 4 +- .../line-chart/line-chart.component.scss | 12 -- .../pie-chart/pie-chart.component.html | 4 +- .../pie-chart/pie-chart.component.scss | 11 +- src/assets/i18n/en.json | 1 + 68 files changed, 572 insertions(+), 576 deletions(-) rename src/app/features/{institutions/pages/admin => admin-institutions}/admin-institutions.component.html (50%) rename src/app/features/{institutions/pages/admin => admin-institutions}/admin-institutions.component.scss (100%) rename src/app/features/{institutions/pages/admin => admin-institutions}/admin-institutions.component.spec.ts (100%) rename src/app/features/{institutions/pages/admin => admin-institutions}/admin-institutions.component.ts (77%) rename src/app/features/{institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.html => admin-institutions/constants/index.ts} (100%) create mode 100644 src/app/features/admin-institutions/constants/resource-tab-option.constant.ts create mode 100644 src/app/features/admin-institutions/mappers/index.ts create mode 100644 src/app/features/admin-institutions/mappers/institution-departments.mapper.ts rename src/app/features/{institutions => admin-institutions}/mappers/institution-summary-index.mapper.ts (60%) rename src/app/features/{institutions => admin-institutions}/mappers/institution-summary-metrics.mapper.ts (79%) rename src/app/features/{institutions => admin-institutions}/models/index.ts (70%) create mode 100644 src/app/features/admin-institutions/models/institution-department.model.ts create mode 100644 src/app/features/admin-institutions/models/institution-departments-json-api.model.ts rename src/app/features/{institutions => admin-institutions}/models/institution-index-value-search-json-api.model.ts (62%) create mode 100644 src/app/features/admin-institutions/models/institution-search-filter.model.ts rename src/app/features/{institutions => admin-institutions}/models/institution-summary-metric.model.ts (100%) rename src/app/features/{institutions => admin-institutions}/models/institution-summary-metrics-json-api.model.ts (68%) create mode 100644 src/app/features/admin-institutions/pages/index.ts rename src/app/features/{institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.scss => admin-institutions/pages/institutions-preprints/institutions-preprints.component.html} (100%) rename src/app/features/{institutions/pages/admin/pages/institutions-projects/institutions-projects.component.html => admin-institutions/pages/institutions-preprints/institutions-preprints.component.scss} (100%) rename src/app/features/{institutions/pages/admin => admin-institutions}/pages/institutions-preprints/institutions-preprints.component.spec.ts (100%) rename src/app/features/{institutions/pages/admin => admin-institutions}/pages/institutions-preprints/institutions-preprints.component.ts (100%) rename src/app/features/{institutions/pages/admin/pages/institutions-projects/institutions-projects.component.scss => admin-institutions/pages/institutions-projects/institutions-projects.component.html} (100%) rename src/app/features/{institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.html => admin-institutions/pages/institutions-projects/institutions-projects.component.scss} (100%) rename src/app/features/{institutions/pages/admin => admin-institutions}/pages/institutions-projects/institutions-projects.component.spec.ts (100%) rename src/app/features/{institutions/pages/admin => admin-institutions}/pages/institutions-projects/institutions-projects.component.ts (100%) rename src/app/features/{institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.scss => admin-institutions/pages/institutions-registrations/institutions-registrations.component.html} (100%) rename src/app/features/{institutions/pages/admin/pages/institutions-users/institutions-users.component.scss => admin-institutions/pages/institutions-registrations/institutions-registrations.component.scss} (100%) rename src/app/features/{institutions/pages/admin => admin-institutions}/pages/institutions-registrations/institutions-registrations.component.spec.ts (100%) rename src/app/features/{institutions/pages/admin => admin-institutions}/pages/institutions-registrations/institutions-registrations.component.ts (100%) rename src/app/features/{institutions/pages/admin => admin-institutions}/pages/institutions-summary/institutions-summary.component.html (98%) rename src/app/features/{institutions/pages/admin => admin-institutions}/pages/institutions-summary/institutions-summary.component.scss (100%) rename src/app/features/{institutions/pages/admin => admin-institutions}/pages/institutions-summary/institutions-summary.component.spec.ts (100%) rename src/app/features/{institutions/pages/admin => admin-institutions}/pages/institutions-summary/institutions-summary.component.ts (96%) rename src/app/features/{institutions/pages/admin => admin-institutions}/pages/institutions-users/institutions-users.component.html (100%) create mode 100644 src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.scss rename src/app/features/{institutions/pages/admin => admin-institutions}/pages/institutions-users/institutions-users.component.spec.ts (100%) rename src/app/features/{institutions/pages/admin => admin-institutions}/pages/institutions-users/institutions-users.component.ts (100%) create mode 100644 src/app/features/admin-institutions/routes.ts rename src/app/features/{institutions => admin-institutions}/services/index.ts (100%) create mode 100644 src/app/features/admin-institutions/services/institutions-admin.service.ts create mode 100644 src/app/features/admin-institutions/services/mock.ts rename src/app/features/{institutions => admin-institutions}/store/index.ts (100%) rename src/app/features/{institutions => admin-institutions}/store/institutions-admin.actions.ts (100%) rename src/app/features/{institutions => admin-institutions}/store/institutions-admin.model.ts (71%) rename src/app/features/{institutions => admin-institutions}/store/institutions-admin.selectors.ts (95%) rename src/app/features/{institutions => admin-institutions}/store/institutions-admin.state.ts (76%) delete mode 100644 src/app/features/institutions/mappers/index.ts delete mode 100644 src/app/features/institutions/models/institution-departments-json-api.model.ts delete mode 100644 src/app/features/institutions/pages/admin/pages/index.ts delete mode 100644 src/app/features/institutions/pages/admin/routes.ts create mode 100644 src/app/features/institutions/pages/institutions-list/institutions-list.component.html create mode 100644 src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts create mode 100644 src/app/features/institutions/pages/institutions-list/institutions-list.component.ts delete mode 100644 src/app/features/institutions/services/institutions-admin.service.ts diff --git a/src/app/features/institutions/pages/admin/admin-institutions.component.html b/src/app/features/admin-institutions/admin-institutions.component.html similarity index 50% rename from src/app/features/institutions/pages/admin/admin-institutions.component.html rename to src/app/features/admin-institutions/admin-institutions.component.html index 9d9173309..6d56b87ed 100644 --- a/src/app/features/institutions/pages/admin/admin-institutions.component.html +++ b/src/app/features/admin-institutions/admin-institutions.component.html @@ -1,6 +1,6 @@
    @if (isInstitutionLoading()) { -
    +
    } @else { @@ -17,16 +17,17 @@

    {{ institution().name }}

    - } - -
    - -
    + + +
    + +
    + }
    diff --git a/src/app/features/institutions/pages/admin/admin-institutions.component.scss b/src/app/features/admin-institutions/admin-institutions.component.scss similarity index 100% rename from src/app/features/institutions/pages/admin/admin-institutions.component.scss rename to src/app/features/admin-institutions/admin-institutions.component.scss diff --git a/src/app/features/institutions/pages/admin/admin-institutions.component.spec.ts b/src/app/features/admin-institutions/admin-institutions.component.spec.ts similarity index 100% rename from src/app/features/institutions/pages/admin/admin-institutions.component.spec.ts rename to src/app/features/admin-institutions/admin-institutions.component.spec.ts diff --git a/src/app/features/institutions/pages/admin/admin-institutions.component.ts b/src/app/features/admin-institutions/admin-institutions.component.ts similarity index 77% rename from src/app/features/institutions/pages/admin/admin-institutions.component.ts rename to src/app/features/admin-institutions/admin-institutions.component.ts index a8bc34b32..d17c7ffc1 100644 --- a/src/app/features/institutions/pages/admin/admin-institutions.component.ts +++ b/src/app/features/admin-institutions/admin-institutions.component.ts @@ -8,15 +8,10 @@ import { NgOptimizedImage } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; import { ActivatedRoute, Router, RouterOutlet } from '@angular/router'; +import { resourceTabOptions } from '@osf/features/admin-institutions/constants/resource-tab-option.constant'; import { LoadingSpinnerComponent } from '@shared/components'; import { FetchInstitutionById, InstitutionsSearchSelectors } from '@shared/stores'; -interface TabOption { - label: string; - value: string; - route: string; -} - @Component({ selector: 'osf-admin-institutions', imports: [TabsModule, TranslateModule, RouterOutlet, NgOptimizedImage, LoadingSpinnerComponent], @@ -37,13 +32,7 @@ export class AdminInstitutionsComponent implements OnInit { selectedTab = 'summary'; - resourceTabOptions: TabOption[] = [ - { label: 'Summary', value: 'summary', route: 'summary' }, - { label: 'Users', value: 'users', route: 'users' }, - { label: 'Projects', value: 'projects', route: 'projects' }, - { label: 'Registrations', value: 'registrations', route: 'registrations' }, - { label: 'Preprints', value: 'preprints', route: 'preprints' }, - ]; + resourceTabOptions = resourceTabOptions; ngOnInit() { const institutionId = this.route.snapshot.params['institution-id']; @@ -60,7 +49,7 @@ export class AdminInstitutionsComponent implements OnInit { this.selectedTab = value; const selectedTab = this.resourceTabOptions.find((tab) => tab.value === value); if (selectedTab) { - this.router.navigate([selectedTab.route], { relativeTo: this.route }); + this.router.navigate([selectedTab.value], { relativeTo: this.route }); } } } diff --git a/src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.html b/src/app/features/admin-institutions/constants/index.ts similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.html rename to src/app/features/admin-institutions/constants/index.ts diff --git a/src/app/features/admin-institutions/constants/resource-tab-option.constant.ts b/src/app/features/admin-institutions/constants/resource-tab-option.constant.ts new file mode 100644 index 000000000..6e9799ac0 --- /dev/null +++ b/src/app/features/admin-institutions/constants/resource-tab-option.constant.ts @@ -0,0 +1,9 @@ +import { CustomOption } from '@shared/models'; + +export const resourceTabOptions: CustomOption[] = [ + { label: 'adminInstitutions.summary.title', value: 'summary' }, + { label: 'common.search.tabs.users', value: 'users' }, + { label: 'common.search.tabs.projects', value: 'projects' }, + { label: 'common.search.tabs.registrations', value: 'registrations' }, + { label: 'common.search.tabs.preprints', value: 'preprints' }, +]; diff --git a/src/app/features/admin-institutions/mappers/index.ts b/src/app/features/admin-institutions/mappers/index.ts new file mode 100644 index 000000000..5c7fed63d --- /dev/null +++ b/src/app/features/admin-institutions/mappers/index.ts @@ -0,0 +1,2 @@ +export { mapInstitutionDepartment, mapInstitutionDepartments } from './institution-departments.mapper'; +export { mapInstitutionSummaryMetrics } from './institution-summary-metrics.mapper'; diff --git a/src/app/features/admin-institutions/mappers/institution-departments.mapper.ts b/src/app/features/admin-institutions/mappers/institution-departments.mapper.ts new file mode 100644 index 000000000..22b0f07df --- /dev/null +++ b/src/app/features/admin-institutions/mappers/institution-departments.mapper.ts @@ -0,0 +1,23 @@ +import { + InstitutionDepartment, + InstitutionDepartmentDataJsonApi, + InstitutionDepartmentsJsonApi, +} from '@osf/features/admin-institutions/models'; + +export function mapInstitutionDepartments(jsonApiData: InstitutionDepartmentsJsonApi): InstitutionDepartment[] { + return jsonApiData.data.map((department: InstitutionDepartmentDataJsonApi) => ({ + id: department.id, + name: department.attributes.name, + numberOfUsers: department.attributes.number_of_users, + selfLink: department.links.self, + })); +} + +export function mapInstitutionDepartment(department: InstitutionDepartmentDataJsonApi): InstitutionDepartment { + return { + id: department.id, + name: department.attributes.name, + numberOfUsers: department.attributes.number_of_users, + selfLink: department.links.self, + }; +} diff --git a/src/app/features/institutions/mappers/institution-summary-index.mapper.ts b/src/app/features/admin-institutions/mappers/institution-summary-index.mapper.ts similarity index 60% rename from src/app/features/institutions/mappers/institution-summary-index.mapper.ts rename to src/app/features/admin-institutions/mappers/institution-summary-index.mapper.ts index 5560342f2..14c55de85 100644 --- a/src/app/features/institutions/mappers/institution-summary-index.mapper.ts +++ b/src/app/features/admin-institutions/mappers/institution-summary-index.mapper.ts @@ -1,9 +1,9 @@ import { - InstitutionIndexCardFilter, + InstitutionIndexCardFilterJsonApi, InstitutionIndexValueSearchIncludedJsonApi, InstitutionSearchFilter, - InstitutionSearchResultCount, -} from '@osf/features/institutions/models'; + InstitutionSearchResultCountJsonApi, +} from '@osf/features/admin-institutions/models'; export function mapIndexCardResults(included: InstitutionIndexValueSearchIncludedJsonApi[]): InstitutionSearchFilter[] { const indexCardMap = included.reduce( @@ -12,8 +12,8 @@ export function mapIndexCardResults(included: InstitutionIndexValueSearchInclude acc[item.id] = { id: item.id, label: - (item as InstitutionIndexCardFilter)?.attributes?.resourceMetadata?.displayLabel?.[0]?.['@value'] || - (item as InstitutionIndexCardFilter)?.attributes?.resourceMetadata?.name?.[0]?.['@value'] || + (item as InstitutionIndexCardFilterJsonApi)?.attributes?.resourceMetadata?.displayLabel?.[0]?.['@value'] || + (item as InstitutionIndexCardFilterJsonApi)?.attributes?.resourceMetadata?.name?.[0]?.['@value'] || item.id, }; } @@ -24,8 +24,8 @@ export function mapIndexCardResults(included: InstitutionIndexValueSearchInclude return included.reduce((result, item) => { if (item.type === 'search-result') { - const indexCardId = (item as InstitutionSearchResultCount).relationships?.indexCard?.data?.id; - const count = (item as InstitutionSearchResultCount).attributes?.cardSearchResultCount ?? 0; + const indexCardId = (item as InstitutionSearchResultCountJsonApi).relationships?.indexCard?.data?.id; + const count = (item as InstitutionSearchResultCountJsonApi).attributes?.cardSearchResultCount ?? 0; const indexCard = indexCardMap[indexCardId]; if (indexCard) { result.push({ diff --git a/src/app/features/institutions/mappers/institution-summary-metrics.mapper.ts b/src/app/features/admin-institutions/mappers/institution-summary-metrics.mapper.ts similarity index 79% rename from src/app/features/institutions/mappers/institution-summary-metrics.mapper.ts rename to src/app/features/admin-institutions/mappers/institution-summary-metrics.mapper.ts index eb808e4b2..c4416f7a8 100644 --- a/src/app/features/institutions/mappers/institution-summary-metrics.mapper.ts +++ b/src/app/features/admin-institutions/mappers/institution-summary-metrics.mapper.ts @@ -1,7 +1,10 @@ -import { InstitutionSummaryMetrics, InstitutionSummaryMetricsAttributes } from '@osf/features/institutions/models'; +import { + InstitutionSummaryMetrics, + InstitutionSummaryMetricsAttributesJsonApi, +} from '@osf/features/admin-institutions/models'; export function mapInstitutionSummaryMetrics( - attributes: InstitutionSummaryMetricsAttributes + attributes: InstitutionSummaryMetricsAttributesJsonApi ): InstitutionSummaryMetrics { return { reportYearmonth: attributes.report_yearmonth, diff --git a/src/app/features/institutions/models/index.ts b/src/app/features/admin-institutions/models/index.ts similarity index 70% rename from src/app/features/institutions/models/index.ts rename to src/app/features/admin-institutions/models/index.ts index 5f23583a9..927d91012 100644 --- a/src/app/features/institutions/models/index.ts +++ b/src/app/features/admin-institutions/models/index.ts @@ -1,4 +1,6 @@ +export * from './institution-department.model'; export * from './institution-departments-json-api.model'; export * from './institution-index-value-search-json-api.model'; +export * from './institution-search-filter.model'; export * from './institution-summary-metric.model'; export * from './institution-summary-metrics-json-api.model'; diff --git a/src/app/features/admin-institutions/models/institution-department.model.ts b/src/app/features/admin-institutions/models/institution-department.model.ts new file mode 100644 index 000000000..f0341bd1d --- /dev/null +++ b/src/app/features/admin-institutions/models/institution-department.model.ts @@ -0,0 +1,6 @@ +export interface InstitutionDepartment { + id: string; + name: string; + numberOfUsers: number; + selfLink: string; +} diff --git a/src/app/features/admin-institutions/models/institution-departments-json-api.model.ts b/src/app/features/admin-institutions/models/institution-departments-json-api.model.ts new file mode 100644 index 000000000..b44db76b7 --- /dev/null +++ b/src/app/features/admin-institutions/models/institution-departments-json-api.model.ts @@ -0,0 +1,19 @@ +export interface InstitutionDepartmentAttributesJsonApi { + name: string; + number_of_users: number; +} + +export interface InstitutionDepartmentLinksJsonApi { + self: string; +} + +export interface InstitutionDepartmentDataJsonApi { + id: string; + type: 'institution-departments'; + attributes: InstitutionDepartmentAttributesJsonApi; + links: InstitutionDepartmentLinksJsonApi; +} + +export interface InstitutionDepartmentsJsonApi { + data: InstitutionDepartmentDataJsonApi[]; +} diff --git a/src/app/features/institutions/models/institution-index-value-search-json-api.model.ts b/src/app/features/admin-institutions/models/institution-index-value-search-json-api.model.ts similarity index 62% rename from src/app/features/institutions/models/institution-index-value-search-json-api.model.ts rename to src/app/features/admin-institutions/models/institution-index-value-search-json-api.model.ts index af48c27f7..c056e112a 100644 --- a/src/app/features/institutions/models/institution-index-value-search-json-api.model.ts +++ b/src/app/features/admin-institutions/models/institution-index-value-search-json-api.model.ts @@ -1,6 +1,6 @@ -import { JsonApiResponse } from '@osf/core/models'; +import { JsonApiResponse } from '@core/models'; -export interface InstitutionSearchResultCount { +export interface InstitutionSearchResultCountJsonApi { attributes: { cardSearchResultCount: number; }; @@ -16,7 +16,7 @@ export interface InstitutionSearchResultCount { }; } -export interface InstitutionIndexCardFilter { +export interface InstitutionIndexCardFilterJsonApi { attributes: { resourceIdentifier: string[]; resourceMetadata: { @@ -29,17 +29,12 @@ export interface InstitutionIndexCardFilter { type: string; } -export type InstitutionIndexValueSearchIncludedJsonApi = InstitutionSearchResultCount | InstitutionIndexCardFilter; +export type InstitutionIndexValueSearchIncludedJsonApi = + | InstitutionSearchResultCountJsonApi + | InstitutionIndexCardFilterJsonApi; export interface InstitutionIndexValueSearchJsonApi extends JsonApiResponse { data: null; included: InstitutionIndexValueSearchIncludedJsonApi[]; } - -export interface InstitutionSearchFilter { - id: string; - label: string; - value: string | number; - count?: number; -} diff --git a/src/app/features/admin-institutions/models/institution-search-filter.model.ts b/src/app/features/admin-institutions/models/institution-search-filter.model.ts new file mode 100644 index 000000000..8eeec21c4 --- /dev/null +++ b/src/app/features/admin-institutions/models/institution-search-filter.model.ts @@ -0,0 +1,6 @@ +export interface InstitutionSearchFilter { + id: string; + label: string; + value: string | number; + count?: number; +} diff --git a/src/app/features/institutions/models/institution-summary-metric.model.ts b/src/app/features/admin-institutions/models/institution-summary-metric.model.ts similarity index 100% rename from src/app/features/institutions/models/institution-summary-metric.model.ts rename to src/app/features/admin-institutions/models/institution-summary-metric.model.ts diff --git a/src/app/features/institutions/models/institution-summary-metrics-json-api.model.ts b/src/app/features/admin-institutions/models/institution-summary-metrics-json-api.model.ts similarity index 68% rename from src/app/features/institutions/models/institution-summary-metrics-json-api.model.ts rename to src/app/features/admin-institutions/models/institution-summary-metrics-json-api.model.ts index 62a39243f..c418e98ac 100644 --- a/src/app/features/institutions/models/institution-summary-metrics-json-api.model.ts +++ b/src/app/features/admin-institutions/models/institution-summary-metrics-json-api.model.ts @@ -1,4 +1,4 @@ -export interface InstitutionSummaryMetricsAttributes { +export interface InstitutionSummaryMetricsAttributesJsonApi { report_yearmonth: string; user_count: number; public_project_count: number; @@ -12,7 +12,7 @@ export interface InstitutionSummaryMetricsAttributes { monthly_active_user_count: number; } -export interface InstitutionSummaryMetricsRelationships { +export interface InstitutionSummaryMetricsRelationshipsJsonApi { user: { data: null; }; @@ -30,16 +30,16 @@ export interface InstitutionSummaryMetricsRelationships { }; } -export interface InstitutionSummaryMetricsData { +export interface InstitutionSummaryMetricsDataJsonApi { id: string; type: 'institution-summary-metrics'; - attributes: InstitutionSummaryMetricsAttributes; - relationships: InstitutionSummaryMetricsRelationships; + attributes: InstitutionSummaryMetricsAttributesJsonApi; + relationships: InstitutionSummaryMetricsRelationshipsJsonApi; links: Record; } export interface InstitutionSummaryMetricsJsonApi { - data: InstitutionSummaryMetricsData; + data: InstitutionSummaryMetricsDataJsonApi; meta: { version: string; }; diff --git a/src/app/features/admin-institutions/pages/index.ts b/src/app/features/admin-institutions/pages/index.ts new file mode 100644 index 000000000..c5679acff --- /dev/null +++ b/src/app/features/admin-institutions/pages/index.ts @@ -0,0 +1,5 @@ +export { InstitutionsPreprintsComponent } from './institutions-preprints/institutions-preprints.component'; +export { InstitutionsProjectsComponent } from './institutions-projects/institutions-projects.component'; +export { InstitutionsRegistrationsComponent } from './institutions-registrations/institutions-registrations.component'; +export { InstitutionsSummaryComponent } from './institutions-summary/institutions-summary.component'; +export { InstitutionsUsersComponent } from './institutions-users/institutions-users.component'; diff --git a/src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.scss b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.html similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.scss rename to src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.html diff --git a/src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.html b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.scss similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.html rename to src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.scss diff --git a/src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.spec.ts b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.spec.ts similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.spec.ts rename to src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.spec.ts diff --git a/src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.ts b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component.ts rename to src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts diff --git a/src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.scss b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.html similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.scss rename to src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.html diff --git a/src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.html b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.scss similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.html rename to src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.scss diff --git a/src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.spec.ts b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.spec.ts similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.spec.ts rename to src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.spec.ts diff --git a/src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.ts b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.ts similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component.ts rename to src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.ts diff --git a/src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.scss b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.html similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.scss rename to src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.html diff --git a/src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.scss b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.scss similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.scss rename to src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.scss diff --git a/src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.spec.ts b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.spec.ts similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.spec.ts rename to src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.spec.ts diff --git a/src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.ts b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component.ts rename to src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts diff --git a/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.html b/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.html similarity index 98% rename from src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.html rename to src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.html index ec7848922..5d2f02e8c 100644 --- a/src/app/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component.html +++ b/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.html @@ -53,7 +53,7 @@
    item.attributes.name || ''); - this.departmentDataset = [{ label: '', data: departments.data.map((item) => item.attributes.number_of_users) }]; + this.departmentLabels = departments.map((item) => item.name || ''); + this.departmentDataset = [{ label: '', data: departments.map((item) => item.numberOfUsers) }]; this.projectsLabels = ['resourceCard.labels.publicProjects', 'adminInstitutions.summary.privateProjects'].map( (el) => this.translateService.instant(el) diff --git a/src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.html b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.html similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.html rename to src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.html diff --git a/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.scss b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.spec.ts b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.spec.ts similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.spec.ts rename to src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.spec.ts diff --git a/src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.ts b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.ts similarity index 100% rename from src/app/features/institutions/pages/admin/pages/institutions-users/institutions-users.component.ts rename to src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.ts diff --git a/src/app/features/admin-institutions/routes.ts b/src/app/features/admin-institutions/routes.ts new file mode 100644 index 000000000..9d697be2e --- /dev/null +++ b/src/app/features/admin-institutions/routes.ts @@ -0,0 +1,50 @@ +import { provideStates } from '@ngxs/store'; + +import { Routes } from '@angular/router'; + +import { + InstitutionsPreprintsComponent, + InstitutionsProjectsComponent, + InstitutionsRegistrationsComponent, + InstitutionsSummaryComponent, + InstitutionsUsersComponent, +} from '@osf/features/admin-institutions/pages'; + +import { AdminInstitutionsComponent } from './admin-institutions.component'; + +import { InstitutionsAdminState } from 'src/app/features/admin-institutions/store'; + +export const routes: Routes = [ + { + path: '', + component: AdminInstitutionsComponent, + providers: [provideStates([InstitutionsAdminState])], + children: [ + { + path: '', + redirectTo: 'summary', + pathMatch: 'full', + }, + { + path: 'summary', + component: InstitutionsSummaryComponent, + }, + { + path: 'users', + component: InstitutionsUsersComponent, + }, + { + path: 'projects', + component: InstitutionsProjectsComponent, + }, + { + path: 'registrations', + component: InstitutionsRegistrationsComponent, + }, + { + path: 'preprints', + component: InstitutionsPreprintsComponent, + }, + ], + }, +]; diff --git a/src/app/features/institutions/services/index.ts b/src/app/features/admin-institutions/services/index.ts similarity index 100% rename from src/app/features/institutions/services/index.ts rename to src/app/features/admin-institutions/services/index.ts diff --git a/src/app/features/admin-institutions/services/institutions-admin.service.ts b/src/app/features/admin-institutions/services/institutions-admin.service.ts new file mode 100644 index 000000000..deb219531 --- /dev/null +++ b/src/app/features/admin-institutions/services/institutions-admin.service.ts @@ -0,0 +1,70 @@ +import { catchError, map, Observable, of } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiService } from '@core/services'; +import { mapIndexCardResults } from '@osf/features/admin-institutions/mappers/institution-summary-index.mapper'; +import { departmens, summaryMetrics } from '@osf/features/admin-institutions/services/mock'; + +import { environment } from '../../../../environments/environment'; +import { + InstitutionDepartment, + InstitutionDepartmentsJsonApi, + InstitutionIndexValueSearchJsonApi, + InstitutionSearchFilter, + InstitutionSummaryMetrics, + InstitutionSummaryMetricsJsonApi, +} from '../models'; + +import { mapInstitutionDepartments, mapInstitutionSummaryMetrics } from 'src/app/features/admin-institutions/mappers'; + +@Injectable({ + providedIn: 'root', +}) +export class InstitutionsAdminService { + hardcodedUrl = 'https://api.test.osf.io/v2'; + + private jsonApiService = inject(JsonApiService); + + fetchDepartments(institutionId: string): Observable { + return this.jsonApiService + .get(`${this.hardcodedUrl}/institutions/${institutionId}/metrics/departments/`) + .pipe( + //TODO: remove mock data + catchError(() => { + return of(departmens as InstitutionDepartmentsJsonApi); + }), + map((res) => mapInstitutionDepartments(res)) + ); + } + + fetchSummary(institutionId: string): Observable { + return this.jsonApiService + .get(`${this.hardcodedUrl}/institutions/${institutionId}/metrics/summary/`) + .pipe( + //TODO: remove mock data + catchError(() => { + return of(summaryMetrics as InstitutionSummaryMetricsJsonApi); + }), + map((result) => mapInstitutionSummaryMetrics(result.data.attributes)) + ); + } + + fetchIndexValueSearch( + institutionId: string, + valueSearchPropertyPath: string, + additionalParams?: Record + ): Observable { + const params: Record = { + //TODO: change here https://test.osf.io to current environment + 'cardSearchFilter[affiliation]': `https://ror.org/05d5mza29,https://test.osf.io/institutions/${institutionId}/`, + valueSearchPropertyPath, + 'page[size]': '10', + ...additionalParams, + }; + + return this.jsonApiService + .get(`${environment.shareDomainUrl}/index-value-search`, params) + .pipe(map((response) => mapIndexCardResults(response?.included))); + } +} diff --git a/src/app/features/admin-institutions/services/mock.ts b/src/app/features/admin-institutions/services/mock.ts new file mode 100644 index 000000000..1538c7dc7 --- /dev/null +++ b/src/app/features/admin-institutions/services/mock.ts @@ -0,0 +1,90 @@ +export const departmens = { + data: [ + { + id: 'cos-N/A', + type: 'institution-departments', + attributes: { + name: 'N/A', + number_of_users: 159, + }, + links: { + self: 'https://api.test.osf.io/v2/institutions/cos/metrics/departments/', + }, + }, + { + id: 'cos-', + type: 'institution-departments', + attributes: { + name: '', + number_of_users: 9, + }, + links: { + self: 'https://api.test.osf.io/v2/institutions/cos/metrics/departments/', + }, + }, + { + id: 'cos-QA', + type: 'institution-departments', + attributes: { + name: 'QA', + number_of_users: 5, + }, + links: { + self: 'https://api.test.osf.io/v2/institutions/cos/metrics/departments/', + }, + }, + ], + meta: { + total: 3, + per_page: 10, + version: '2.20', + }, + links: { + self: 'https://api.test.osf.io/v2/institutions/cos/metrics/departments/', + first: null, + last: null, + prev: null, + next: null, + }, +}; + +export const summaryMetrics = { + data: { + id: 'cos', + type: 'institution-summary-metrics', + attributes: { + report_yearmonth: '2025-06', + user_count: 173, + public_project_count: 174, + private_project_count: 839, + public_registration_count: 360, + embargoed_registration_count: 76, + published_preprint_count: 1464, + public_file_count: 7088, + storage_byte_count: 12365253682, + monthly_logged_in_user_count: 14, + monthly_active_user_count: 15, + }, + relationships: { + user: { + data: null, + }, + institution: { + links: { + related: { + href: 'https://api.test.osf.io/v2/institutions/cos/', + meta: {}, + }, + }, + data: { + id: 'cos', + type: 'institutions', + }, + }, + }, + links: {}, + }, + meta: { + version: '2.20', + }, +}; diff --git a/src/app/features/institutions/store/index.ts b/src/app/features/admin-institutions/store/index.ts similarity index 100% rename from src/app/features/institutions/store/index.ts rename to src/app/features/admin-institutions/store/index.ts diff --git a/src/app/features/institutions/store/institutions-admin.actions.ts b/src/app/features/admin-institutions/store/institutions-admin.actions.ts similarity index 100% rename from src/app/features/institutions/store/institutions-admin.actions.ts rename to src/app/features/admin-institutions/store/institutions-admin.actions.ts diff --git a/src/app/features/institutions/store/institutions-admin.model.ts b/src/app/features/admin-institutions/store/institutions-admin.model.ts similarity index 71% rename from src/app/features/institutions/store/institutions-admin.model.ts rename to src/app/features/admin-institutions/store/institutions-admin.model.ts index 1e2488931..5c30eb2dd 100644 --- a/src/app/features/institutions/store/institutions-admin.model.ts +++ b/src/app/features/admin-institutions/store/institutions-admin.model.ts @@ -1,9 +1,9 @@ import { AsyncStateModel } from '@shared/models'; -import { InstitutionDepartmentsJsonApi, InstitutionSearchFilter, InstitutionSummaryMetrics } from '../models'; +import { InstitutionDepartment, InstitutionSearchFilter, InstitutionSummaryMetrics } from '../models'; export interface InstitutionsAdminModel { - departments: AsyncStateModel; + departments: AsyncStateModel; summaryMetrics: AsyncStateModel; hasOsfAddonSearch: AsyncStateModel; storageRegionSearch: AsyncStateModel; diff --git a/src/app/features/institutions/store/institutions-admin.selectors.ts b/src/app/features/admin-institutions/store/institutions-admin.selectors.ts similarity index 95% rename from src/app/features/institutions/store/institutions-admin.selectors.ts rename to src/app/features/admin-institutions/store/institutions-admin.selectors.ts index 12212411d..89a4558c4 100644 --- a/src/app/features/institutions/store/institutions-admin.selectors.ts +++ b/src/app/features/admin-institutions/store/institutions-admin.selectors.ts @@ -1,13 +1,13 @@ import { Selector } from '@ngxs/store'; -import { InstitutionDepartmentsJsonApi, InstitutionSearchFilter, InstitutionSummaryMetrics } from '../models'; +import { InstitutionDepartment, InstitutionSearchFilter, InstitutionSummaryMetrics } from '../models'; import { InstitutionsAdminModel } from './institutions-admin.model'; import { InstitutionsAdminState } from './institutions-admin.state'; export class InstitutionsAdminSelectors { @Selector([InstitutionsAdminState]) - static getDepartments(state: InstitutionsAdminModel): InstitutionDepartmentsJsonApi { + static getDepartments(state: InstitutionsAdminModel): InstitutionDepartment[] { return state.departments.data; } diff --git a/src/app/features/institutions/store/institutions-admin.state.ts b/src/app/features/admin-institutions/store/institutions-admin.state.ts similarity index 76% rename from src/app/features/institutions/store/institutions-admin.state.ts rename to src/app/features/admin-institutions/store/institutions-admin.state.ts index 336440b08..016e75b8c 100644 --- a/src/app/features/institutions/store/institutions-admin.state.ts +++ b/src/app/features/admin-institutions/store/institutions-admin.state.ts @@ -4,12 +4,7 @@ import { catchError, tap, throwError } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { - InstitutionDepartmentsJsonApi, - InstitutionSearchFilter, - InstitutionSummaryMetrics, - InstitutionSummaryMetricsJsonApi, -} from '../models'; +import { InstitutionSummaryMetrics } from '../models'; import { InstitutionsAdminService } from '../services/institutions-admin.service'; import { @@ -21,61 +16,14 @@ import { } from './institutions-admin.actions'; import { InstitutionsAdminModel } from './institutions-admin.model'; -const createEmptyDepartments = (): InstitutionDepartmentsJsonApi => ({ - data: [], -}); - -const createEmptySummaryMetrics = (): InstitutionSummaryMetricsJsonApi => ({ - data: { - id: '', - type: 'institution-summary-metrics', - attributes: { - report_yearmonth: '', - user_count: 0, - public_project_count: 0, - private_project_count: 0, - public_registration_count: 0, - embargoed_registration_count: 0, - published_preprint_count: 0, - public_file_count: 0, - storage_byte_count: 0, - monthly_logged_in_user_count: 0, - monthly_active_user_count: 0, - }, - relationships: { - user: { - data: null, - }, - institution: { - links: { - related: { - href: '', - meta: {}, - }, - }, - data: { - id: '', - type: 'institutions', - }, - }, - }, - links: {}, - }, - meta: { - version: '', - }, -}); - -const createEmptySearchFilters = (): InstitutionSearchFilter[] => []; - @State({ name: 'institutionsAdmin', defaults: { - departments: { data: createEmptyDepartments(), isLoading: false, error: null }, + departments: { data: [], isLoading: false, error: null }, summaryMetrics: { data: {} as InstitutionSummaryMetrics, isLoading: false, error: null }, - hasOsfAddonSearch: { data: createEmptySearchFilters(), isLoading: false, error: null }, - storageRegionSearch: { data: createEmptySearchFilters(), isLoading: false, error: null }, - searchResults: { data: createEmptySearchFilters(), isLoading: false, error: null }, + hasOsfAddonSearch: { data: [], isLoading: false, error: null }, + storageRegionSearch: { data: [], isLoading: false, error: null }, + searchResults: { data: [], isLoading: false, error: null }, selectedInstitutionId: null, currentSearchPropertyPath: null, }, diff --git a/src/app/features/institutions/institutions.component.html b/src/app/features/institutions/institutions.component.html index 78b251309..67e7bd4cd 100644 --- a/src/app/features/institutions/institutions.component.html +++ b/src/app/features/institutions/institutions.component.html @@ -1,48 +1 @@ -
    - - - @if (institutionsLoading()) { -
    - -
    - } @else { -
    - - -
    - @for (institution of institutions(); track $index) { - -
    - - -

    {{ institution.name }}

    -
    -
    - } - - @if (!institutions().length) { -

    {{ 'common.search.noResultsFound' | translate }}

    - } -
    - - @if (totalInstitutionsCount() > 10) { - - } -
    - } -
    + diff --git a/src/app/features/institutions/institutions.component.ts b/src/app/features/institutions/institutions.component.ts index dba3a099d..0d0687196 100644 --- a/src/app/features/institutions/institutions.component.ts +++ b/src/app/features/institutions/institutions.component.ts @@ -1,134 +1,10 @@ -import { createDispatchMap, select } from '@ngxs/store'; - -import { TranslatePipe } from '@ngx-translate/core'; - -import { PaginatorState } from 'primeng/paginator'; - -import { debounceTime, distinctUntilChanged } from 'rxjs'; - -import { NgOptimizedImage } from '@angular/common'; -import { - ChangeDetectionStrategy, - Component, - DestroyRef, - effect, - HostBinding, - inject, - signal, - untracked, -} from '@angular/core'; -import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; -import { FormControl } from '@angular/forms'; -import { ActivatedRoute, Router, RouterLink } from '@angular/router'; - -import { parseQueryFilterParams } from '@core/helpers'; -import { - CustomPaginatorComponent, - LoadingSpinnerComponent, - SearchInputComponent, - SubHeaderComponent, -} from '@shared/components'; -import { TABLE_PARAMS } from '@shared/constants'; -import { QueryParams } from '@shared/models'; -import { FetchInstitutions, InstitutionsSelectors } from '@shared/stores'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; @Component({ selector: 'osf-institutions', - imports: [ - SubHeaderComponent, - TranslatePipe, - SearchInputComponent, - NgOptimizedImage, - CustomPaginatorComponent, - LoadingSpinnerComponent, - RouterLink, - ], + imports: [RouterOutlet], templateUrl: './institutions.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class InstitutionsComponent { - @HostBinding('class') classes = 'flex-1 flex flex-column w-full'; - - private readonly route = inject(ActivatedRoute); - private readonly router = inject(Router); - private readonly destroyRef = inject(DestroyRef); - - private readonly actions = createDispatchMap({ getInstitutions: FetchInstitutions }); - - searchControl = new FormControl(''); - - queryParams = toSignal(this.route.queryParams); - currentPage = signal(1); - currentPageSize = signal(TABLE_PARAMS.rows); - first = signal(0); - - institutions = select(InstitutionsSelectors.getInstitutions); - totalInstitutionsCount = select(InstitutionsSelectors.getInstitutionsTotalCount); - institutionsLoading = select(InstitutionsSelectors.isInstitutionsLoading); - - constructor() { - this.setupQueryParamsEffect(); - this.setupSearchSubscription(); - } - - onPageChange(event: PaginatorState): void { - this.currentPage.set(event.page ? this.currentPage() + 1 : 1); - this.first.set(event.first ?? 0); - this.updateQueryParams({ - page: this.currentPage(), - size: event.rows, - }); - } - - private setupQueryParamsEffect(): void { - effect(() => { - const rawQueryParams = this.queryParams(); - if (!rawQueryParams) return; - - const parsedQueryParams = parseQueryFilterParams(rawQueryParams); - - this.updateComponentState(parsedQueryParams); - - this.actions.getInstitutions(parsedQueryParams.page, parsedQueryParams.size, parsedQueryParams.search); - }); - } - - private updateQueryParams(updates: Partial): void { - const queryParams: Record = {}; - - if ('page' in updates) { - queryParams['page'] = updates.page!.toString(); - } - if ('size' in updates) { - queryParams['size'] = updates.size!.toString(); - } - if ('search' in updates) { - queryParams['search'] = updates.search || undefined; - } - - this.router.navigate([], { - relativeTo: this.route, - queryParams, - queryParamsHandling: 'merge', - }); - } - - private setupSearchSubscription(): void { - this.searchControl.valueChanges - .pipe(debounceTime(300), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) - .subscribe((searchControl) => { - this.updateQueryParams({ - search: searchControl ?? '', - page: 1, - }); - }); - } - - private updateComponentState(params: QueryParams): void { - untracked(() => { - this.currentPage.set(params.page); - this.currentPageSize.set(params.size); - this.searchControl.setValue(params.search); - }); - } -} +export class InstitutionsComponent {} diff --git a/src/app/features/institutions/institutions.routes.ts b/src/app/features/institutions/institutions.routes.ts index 44082d7e8..8fef4a89e 100644 --- a/src/app/features/institutions/institutions.routes.ts +++ b/src/app/features/institutions/institutions.routes.ts @@ -5,20 +5,27 @@ import { Routes } from '@angular/router'; import { InstitutionsComponent } from '@osf/features/institutions/institutions.component'; import { InstitutionsSearchState } from '@shared/stores'; -import { InstitutionsSearchComponent } from './pages'; +import { InstitutionsListComponent, InstitutionsSearchComponent } from './pages'; export const routes: Routes = [ { path: '', component: InstitutionsComponent, - }, - { - path: ':institution-id', - component: InstitutionsSearchComponent, - providers: [provideStates([InstitutionsSearchState])], - }, - { - path: ':institution-id/dashboard', - loadChildren: () => import('./pages/admin/routes').then((inst) => inst.routes), + children: [ + { + path: '', + pathMatch: 'full', + component: InstitutionsListComponent, + }, + { + path: ':institution-id', + component: InstitutionsSearchComponent, + providers: [provideStates([InstitutionsSearchState])], + }, + { + path: ':institution-id/dashboard', + loadChildren: () => import('../admin-institutions/routes').then((inst) => inst.routes), + }, + ], }, ]; diff --git a/src/app/features/institutions/mappers/index.ts b/src/app/features/institutions/mappers/index.ts deleted file mode 100644 index 9d97e869f..000000000 --- a/src/app/features/institutions/mappers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { mapInstitutionSummaryMetrics } from './institution-summary-metrics.mapper'; diff --git a/src/app/features/institutions/models/institution-departments-json-api.model.ts b/src/app/features/institutions/models/institution-departments-json-api.model.ts deleted file mode 100644 index 6ea8fdf60..000000000 --- a/src/app/features/institutions/models/institution-departments-json-api.model.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface InstitutionDepartmentAttributes { - name: string; - number_of_users: number; -} - -export interface InstitutionDepartmentLinks { - self: string; -} - -export interface InstitutionDepartmentDataJsonAPi { - id: string; - type: 'institution-departments'; - attributes: InstitutionDepartmentAttributes; - links: InstitutionDepartmentLinks; -} - -export interface InstitutionDepartmentsJsonApi { - data: InstitutionDepartmentDataJsonAPi[]; -} diff --git a/src/app/features/institutions/pages/admin/pages/index.ts b/src/app/features/institutions/pages/admin/pages/index.ts deleted file mode 100644 index cd826f24c..000000000 --- a/src/app/features/institutions/pages/admin/pages/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { InstitutionsSummaryComponent } from './institutions-summary/institutions-summary.component'; diff --git a/src/app/features/institutions/pages/admin/routes.ts b/src/app/features/institutions/pages/admin/routes.ts deleted file mode 100644 index ec21441ba..000000000 --- a/src/app/features/institutions/pages/admin/routes.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { provideStates } from '@ngxs/store'; - -import { Routes } from '@angular/router'; - -import { AdminInstitutionsComponent } from '@osf/features/institutions/pages/admin/admin-institutions.component'; -import { InstitutionsPreprintsComponent } from '@osf/features/institutions/pages/admin/pages/institutions-preprints/institutions-preprints.component'; -import { InstitutionsProjectsComponent } from '@osf/features/institutions/pages/admin/pages/institutions-projects/institutions-projects.component'; -import { InstitutionsRegistrationsComponent } from '@osf/features/institutions/pages/admin/pages/institutions-registrations/institutions-registrations.component'; -import { InstitutionsSummaryComponent } from '@osf/features/institutions/pages/admin/pages/institutions-summary/institutions-summary.component'; -import { InstitutionsUsersComponent } from '@osf/features/institutions/pages/admin/pages/institutions-users/institutions-users.component'; -import { InstitutionsAdminState } from '@osf/features/institutions/store'; - -export const routes: Routes = [ - { - path: '', - component: AdminInstitutionsComponent, - providers: [provideStates([InstitutionsAdminState])], - children: [ - { - path: '', - redirectTo: 'summary', - pathMatch: 'full', - }, - { - path: 'summary', - component: InstitutionsSummaryComponent, - }, - { - path: 'users', - component: InstitutionsUsersComponent, - }, - { - path: 'projects', - component: InstitutionsProjectsComponent, - }, - { - path: 'registrations', - component: InstitutionsRegistrationsComponent, - }, - { - path: 'preprints', - component: InstitutionsPreprintsComponent, - }, - ], - }, -]; diff --git a/src/app/features/institutions/pages/index.ts b/src/app/features/institutions/pages/index.ts index f189ec9bc..c782d130d 100644 --- a/src/app/features/institutions/pages/index.ts +++ b/src/app/features/institutions/pages/index.ts @@ -1 +1,2 @@ +export { InstitutionsListComponent } from './institutions-list/institutions-list.component'; export { InstitutionsSearchComponent } from './institutions-search/institutions-search.component'; diff --git a/src/app/features/institutions/pages/institutions-list/institutions-list.component.html b/src/app/features/institutions/pages/institutions-list/institutions-list.component.html new file mode 100644 index 000000000..78b251309 --- /dev/null +++ b/src/app/features/institutions/pages/institutions-list/institutions-list.component.html @@ -0,0 +1,48 @@ +
    + + + @if (institutionsLoading()) { +
    + +
    + } @else { +
    + + +
    + @for (institution of institutions(); track $index) { + +
    + + +

    {{ institution.name }}

    +
    +
    + } + + @if (!institutions().length) { +

    {{ 'common.search.noResultsFound' | translate }}

    + } +
    + + @if (totalInstitutionsCount() > 10) { + + } +
    + } +
    diff --git a/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts b/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts new file mode 100644 index 000000000..123c49bb9 --- /dev/null +++ b/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InstitutionsListComponent } from './institutions-list.component'; + +describe('InstitutionsListComponent', () => { + let component: InstitutionsListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InstitutionsListComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(InstitutionsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/institutions/pages/institutions-list/institutions-list.component.ts b/src/app/features/institutions/pages/institutions-list/institutions-list.component.ts new file mode 100644 index 000000000..46a1f0b32 --- /dev/null +++ b/src/app/features/institutions/pages/institutions-list/institutions-list.component.ts @@ -0,0 +1,134 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { PaginatorState } from 'primeng/paginator'; + +import { debounceTime, distinctUntilChanged } from 'rxjs'; + +import { NgOptimizedImage } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + effect, + HostBinding, + inject, + signal, + untracked, +} from '@angular/core'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; +import { FormControl } from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; + +import { parseQueryFilterParams } from '@core/helpers'; +import { + CustomPaginatorComponent, + LoadingSpinnerComponent, + SearchInputComponent, + SubHeaderComponent, +} from '@shared/components'; +import { TABLE_PARAMS } from '@shared/constants'; +import { QueryParams } from '@shared/models'; +import { FetchInstitutions, InstitutionsSelectors } from '@shared/stores'; + +@Component({ + selector: 'osf-institutions-list', + imports: [ + SubHeaderComponent, + TranslatePipe, + SearchInputComponent, + NgOptimizedImage, + CustomPaginatorComponent, + LoadingSpinnerComponent, + RouterLink, + ], + templateUrl: './institutions-list.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class InstitutionsListComponent { + @HostBinding('class') classes = 'flex-1 flex flex-column w-full'; + + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly destroyRef = inject(DestroyRef); + + private readonly actions = createDispatchMap({ getInstitutions: FetchInstitutions }); + + searchControl = new FormControl(''); + + queryParams = toSignal(this.route.queryParams); + currentPage = signal(1); + currentPageSize = signal(TABLE_PARAMS.rows); + first = signal(0); + + institutions = select(InstitutionsSelectors.getInstitutions); + totalInstitutionsCount = select(InstitutionsSelectors.getInstitutionsTotalCount); + institutionsLoading = select(InstitutionsSelectors.isInstitutionsLoading); + + constructor() { + this.setupQueryParamsEffect(); + this.setupSearchSubscription(); + } + + onPageChange(event: PaginatorState): void { + this.currentPage.set(event.page ? this.currentPage() + 1 : 1); + this.first.set(event.first ?? 0); + this.updateQueryParams({ + page: this.currentPage(), + size: event.rows, + }); + } + + private setupQueryParamsEffect(): void { + effect(() => { + const rawQueryParams = this.queryParams(); + if (!rawQueryParams) return; + + const parsedQueryParams = parseQueryFilterParams(rawQueryParams); + + this.updateComponentState(parsedQueryParams); + + this.actions.getInstitutions(parsedQueryParams.page, parsedQueryParams.size, parsedQueryParams.search); + }); + } + + private updateQueryParams(updates: Partial): void { + const queryParams: Record = {}; + + if ('page' in updates) { + queryParams['page'] = updates.page!.toString(); + } + if ('size' in updates) { + queryParams['size'] = updates.size!.toString(); + } + if ('search' in updates) { + queryParams['search'] = updates.search || undefined; + } + + this.router.navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling: 'merge', + }); + } + + private setupSearchSubscription(): void { + this.searchControl.valueChanges + .pipe(debounceTime(300), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) + .subscribe((searchControl) => { + this.updateQueryParams({ + search: searchControl ?? '', + page: 1, + }); + }); + } + + private updateComponentState(params: QueryParams): void { + untracked(() => { + this.currentPage.set(params.page); + this.currentPageSize.set(params.size); + this.searchControl.setValue(params.search); + }); + } +} diff --git a/src/app/features/institutions/services/institutions-admin.service.ts b/src/app/features/institutions/services/institutions-admin.service.ts deleted file mode 100644 index 745a9e34b..000000000 --- a/src/app/features/institutions/services/institutions-admin.service.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { catchError, map, Observable, of } from 'rxjs'; - -import { inject, Injectable } from '@angular/core'; - -import { JsonApiService } from '@osf/core/services'; -import { mapInstitutionSummaryMetrics } from '@osf/features/institutions/mappers'; -import { mapIndexCardResults } from '@osf/features/institutions/mappers/institution-summary-index.mapper'; - -import { - InstitutionDepartmentsJsonApi, - InstitutionIndexValueSearchJsonApi, - InstitutionSearchFilter, - InstitutionSummaryMetrics, - InstitutionSummaryMetricsJsonApi, -} from '../models'; - -import { environment } from 'src/environments/environment'; - -@Injectable({ - providedIn: 'root', -}) -export class InstitutionsAdminService { - hardcodedUrl = 'https://api.test.osf.io/v2'; - - private jsonApiService = inject(JsonApiService); - - fetchDepartments(institutionId: string): Observable { - return this.jsonApiService - .get(`${this.hardcodedUrl}/institutions/${institutionId}/metrics/departments/`) - .pipe( - //todo: remove mock data - catchError((err) => { - console.warn('Departments API error, returning mock data:', err); - return of({ - data: [ - { - id: 'cos-N/A', - type: 'institution-departments', - attributes: { - name: 'N/A', - number_of_users: 159, - }, - links: { - self: 'https://api.test.osf.io/v2/institutions/cos/metrics/departments/', - }, - }, - { - id: 'cos-', - type: 'institution-departments', - attributes: { - name: '', - number_of_users: 9, - }, - links: { - self: 'https://api.test.osf.io/v2/institutions/cos/metrics/departments/', - }, - }, - { - id: 'cos-QA', - type: 'institution-departments', - attributes: { - name: 'QA', - number_of_users: 5, - }, - links: { - self: 'https://api.test.osf.io/v2/institutions/cos/metrics/departments/', - }, - }, - ], - meta: { - total: 3, - per_page: 10, - version: '2.20', - }, - links: { - self: 'https://api.test.osf.io/v2/institutions/cos/metrics/departments/', - first: null, - last: null, - prev: null, - next: null, - }, - } as InstitutionDepartmentsJsonApi); - }) - ); - } - - fetchSummary(institutionId: string): Observable { - return this.jsonApiService - .get(`${this.hardcodedUrl}/institutions/${institutionId}/metrics/summary/`) - .pipe( - //todo: remove mock data - catchError((err) => { - console.warn('Summary API error, returning mock data:', err); - return of({ - data: { - id: 'cos', - type: 'institution-summary-metrics', - attributes: { - report_yearmonth: '2025-06', - user_count: 173, - public_project_count: 174, - private_project_count: 839, - public_registration_count: 360, - embargoed_registration_count: 76, - published_preprint_count: 1464, - public_file_count: 7088, - storage_byte_count: 12365253682, - monthly_logged_in_user_count: 14, - monthly_active_user_count: 15, - }, - relationships: { - user: { - data: null, - }, - institution: { - links: { - related: { - href: 'https://api.test.osf.io/v2/institutions/cos/', - meta: {}, - }, - }, - data: { - id: 'cos', - type: 'institutions', - }, - }, - }, - links: {}, - }, - meta: { - version: '2.20', - }, - } as InstitutionSummaryMetricsJsonApi); - }), - map((result) => mapInstitutionSummaryMetrics(result.data.attributes)) - ); - } - - fetchIndexValueSearch( - institutionId: string, - valueSearchPropertyPath: string, - additionalParams?: Record - ): Observable { - const params: Record = { - //todo: change here https://test.osf.io to current environment - 'cardSearchFilter[affiliation]': `https://ror.org/05d5mza29,https://test.osf.io/institutions/${institutionId}/`, - valueSearchPropertyPath, - 'page[size]': '10', - ...additionalParams, - }; - - return this.jsonApiService - .get(`${environment.shareDomainUrl}/index-value-search`, params) - .pipe(map((response) => mapIndexCardResults(response?.included))); - } -} diff --git a/src/app/shared/components/bar-chart/bar-chart.component.html b/src/app/shared/components/bar-chart/bar-chart.component.html index 11d299c7a..f9c424f91 100644 --- a/src/app/shared/components/bar-chart/bar-chart.component.html +++ b/src/app/shared/components/bar-chart/bar-chart.component.html @@ -1,5 +1,5 @@ @if (!showExpandedSection()) { -

    {{ title() | translate }}

    +

    {{ title() | translate }}

    } @if (isLoading()) { @@ -7,7 +7,7 @@

    {{ title() | translate }}

    } @else {
    - +
    @if (showExpandedSection()) { diff --git a/src/app/shared/components/bar-chart/bar-chart.component.scss b/src/app/shared/components/bar-chart/bar-chart.component.scss index bcbc327cf..478abc91c 100644 --- a/src/app/shared/components/bar-chart/bar-chart.component.scss +++ b/src/app/shared/components/bar-chart/bar-chart.component.scss @@ -1,15 +1,4 @@ :host { - // display: block; - // width: 100%; -} - -.chart-title { - font-size: 1.7rem; -} - -.chart { display: block; - width: 100%; height: 100%; - margin-top: 1.7rem; } diff --git a/src/app/shared/components/doughnut-chart/doughnut-chart.component.html b/src/app/shared/components/doughnut-chart/doughnut-chart.component.html index a10489f3e..e8d3f10e2 100644 --- a/src/app/shared/components/doughnut-chart/doughnut-chart.component.html +++ b/src/app/shared/components/doughnut-chart/doughnut-chart.component.html @@ -1,5 +1,5 @@ @if (!showExpandedSection()) { -

    {{ title() | translate }}

    +

    {{ title() | translate }}

    } @if (isLoading()) { @@ -7,7 +7,7 @@

    {{ title() | translate }}

    } @else {
    - +
    @if (showExpandedSection()) { diff --git a/src/app/shared/components/doughnut-chart/doughnut-chart.component.scss b/src/app/shared/components/doughnut-chart/doughnut-chart.component.scss index 26cf0932b..478abc91c 100644 --- a/src/app/shared/components/doughnut-chart/doughnut-chart.component.scss +++ b/src/app/shared/components/doughnut-chart/doughnut-chart.component.scss @@ -2,12 +2,3 @@ display: block; height: 100%; } - -.chart-title { - font-size: 1.7rem; -} - -.chart { - display: block; - margin-top: 1.7rem; -} diff --git a/src/app/shared/components/line-chart/line-chart.component.html b/src/app/shared/components/line-chart/line-chart.component.html index ff756e454..961d8bf4d 100644 --- a/src/app/shared/components/line-chart/line-chart.component.html +++ b/src/app/shared/components/line-chart/line-chart.component.html @@ -1,9 +1,9 @@ -

    {{ title() | translate }}

    +

    {{ title() | translate }}

    @if (isLoading()) { } @else {
    - +
    } diff --git a/src/app/shared/components/line-chart/line-chart.component.scss b/src/app/shared/components/line-chart/line-chart.component.scss index bcbc327cf..5d4e87f30 100644 --- a/src/app/shared/components/line-chart/line-chart.component.scss +++ b/src/app/shared/components/line-chart/line-chart.component.scss @@ -1,15 +1,3 @@ :host { - // display: block; - // width: 100%; -} - -.chart-title { - font-size: 1.7rem; -} - -.chart { display: block; - width: 100%; - height: 100%; - margin-top: 1.7rem; } diff --git a/src/app/shared/components/pie-chart/pie-chart.component.html b/src/app/shared/components/pie-chart/pie-chart.component.html index 2491f813b..48c7addd7 100644 --- a/src/app/shared/components/pie-chart/pie-chart.component.html +++ b/src/app/shared/components/pie-chart/pie-chart.component.html @@ -1,9 +1,9 @@ -

    {{ title() | translate }}

    +

    {{ title() | translate }}

    @if (isLoading()) { } @else {
    - +
    } diff --git a/src/app/shared/components/pie-chart/pie-chart.component.scss b/src/app/shared/components/pie-chart/pie-chart.component.scss index db8aeaa21..478abc91c 100644 --- a/src/app/shared/components/pie-chart/pie-chart.component.scss +++ b/src/app/shared/components/pie-chart/pie-chart.component.scss @@ -1,13 +1,4 @@ :host { display: block; - // width: 100%; -} - -.chart-title { - font-size: 1.7rem; -} - -.chart { - display: block; - margin-top: 1.7rem; + height: 100%; } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index c55ac76f0..7624a053a 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1769,6 +1769,7 @@ }, "adminInstitutions": { "summary": { + "title": "Summary", "totalUsersByDepartment": "Total Users by Department", "publicPrivateProjects": "Public vs Private Projects", "publicEmbargoedRegistrations": "Public vs Embargoed Registrations",