Skip to content

Commit 5db7dd3

Browse files
committed
feat(admin-institutions): add last updated on institution summary tab
1 parent 358cbf5 commit 5db7dd3

File tree

6 files changed

+134
-1
lines changed

6 files changed

+134
-1
lines changed

src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
</div>
55
} @else {
66
<div class="w-full">
7+
@if (summaryMetrics()?.reportYearmonth) {
8+
<div class="last-updated-section mb-3">
9+
<span class="last-updated-label">{{ 'adminInstitutions.summary.lastUpdated' | translate }}:</span>
10+
<span class="last-updated-date">{{ summaryMetrics()!.reportYearmonth | reportDate }}</span>
11+
</div>
12+
}
13+
714
<div class="flex flex-wrap mt-4 gap-3 md:gap-4">
815
@for (item of statisticsData; track $index) {
916
<osf-statistic-card class="width-25" [label]="item.label | translate" [value]="item.value" />

src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.scss

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,21 @@
2525
width: 100%;
2626
}
2727
}
28+
29+
.last-updated-section {
30+
display: flex;
31+
align-items: center;
32+
gap: 0.5rem;
33+
font-size: 0.875rem; // 14px
34+
color: var(--text-color-secondary);
35+
padding: 0.75rem 0;
36+
37+
.last-updated-label {
38+
font-weight: 600;
39+
}
40+
41+
.last-updated-date {
42+
color: var(--text-color);
43+
font-weight: 500;
44+
}
45+
}

src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/
1010
import { StatisticCardComponent } from '@osf/shared/components/statistic-card/statistic-card.component';
1111
import { DatasetInput } from '@osf/shared/models/charts/dataset-input';
1212
import { SelectOption } from '@osf/shared/models/select-option.model';
13+
import { ReportDatePipe } from '@osf/shared/pipes/report-date.pipe';
1314
import { DoughnutChartComponent } from '@shared/components/doughnut-chart/doughnut-chart.component';
1415

1516
import {
@@ -23,7 +24,14 @@ import {
2324

2425
@Component({
2526
selector: 'osf-institutions-summary',
26-
imports: [StatisticCardComponent, LoadingSpinnerComponent, DoughnutChartComponent, BarChartComponent, TranslatePipe],
27+
imports: [
28+
StatisticCardComponent,
29+
LoadingSpinnerComponent,
30+
DoughnutChartComponent,
31+
BarChartComponent,
32+
TranslatePipe,
33+
ReportDatePipe,
34+
],
2735
templateUrl: './institutions-summary.component.html',
2836
styleUrl: './institutions-summary.component.scss',
2937
changeDetection: ChangeDetectionStrategy.OnPush,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { ReportDatePipe } from './report-date.pipe';
2+
3+
describe('ReportDatePipe', () => {
4+
let pipe: ReportDatePipe;
5+
6+
beforeEach(() => {
7+
pipe = new ReportDatePipe();
8+
});
9+
10+
it('should create an instance', () => {
11+
expect(pipe).toBeTruthy();
12+
});
13+
14+
it('should transform "2024-08" to "August 1, 2024"', () => {
15+
expect(pipe.transform('2024-08')).toBe('August 1, 2024');
16+
});
17+
18+
it('should transform "2024-12" to "December 1, 2024"', () => {
19+
expect(pipe.transform('2024-12')).toBe('December 1, 2024');
20+
});
21+
22+
it('should transform "2025-01" to "January 1, 2025"', () => {
23+
expect(pipe.transform('2025-01')).toBe('January 1, 2025');
24+
});
25+
26+
it('should return empty string for null', () => {
27+
expect(pipe.transform(null)).toBe('');
28+
});
29+
30+
it('should return empty string for undefined', () => {
31+
expect(pipe.transform(undefined)).toBe('');
32+
});
33+
34+
it('should return empty string for empty string', () => {
35+
expect(pipe.transform('')).toBe('');
36+
});
37+
38+
it('should return empty string for invalid format', () => {
39+
expect(pipe.transform('invalid')).toBe('');
40+
expect(pipe.transform('2024')).toBe('');
41+
expect(pipe.transform('2024-')).toBe('');
42+
});
43+
44+
it('should return empty string for invalid month', () => {
45+
expect(pipe.transform('2024-13')).toBe('');
46+
expect(pipe.transform('2024-00')).toBe('');
47+
});
48+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Pipe, PipeTransform } from '@angular/core';
2+
3+
/**
4+
* Transforms a report yearmonth string from "YYYY-MM" format to "Month Day, Year" format
5+
* Example: "2024-08" -> "August 1, 2024"
6+
*/
7+
@Pipe({
8+
name: 'reportDate',
9+
standalone: true,
10+
})
11+
export class ReportDatePipe implements PipeTransform {
12+
private readonly monthNames = [
13+
'January',
14+
'February',
15+
'March',
16+
'April',
17+
'May',
18+
'June',
19+
'July',
20+
'August',
21+
'September',
22+
'October',
23+
'November',
24+
'December',
25+
];
26+
27+
transform(yearMonth: string | null | undefined): string {
28+
if (!yearMonth) {
29+
return '';
30+
}
31+
32+
// Input format: "YYYY-MM" (e.g., "2024-08")
33+
const [yearStr, monthStr] = yearMonth.split('-');
34+
35+
if (!yearStr || !monthStr) {
36+
return '';
37+
}
38+
39+
const year = parseInt(yearStr, 10);
40+
const month = parseInt(monthStr, 10);
41+
42+
if (isNaN(year) || isNaN(month) || month < 1 || month > 12) {
43+
return '';
44+
}
45+
46+
const monthName = this.monthNames[month - 1];
47+
48+
// Output format: "Month 1, Year" (e.g., "August 1, 2024")
49+
return `${monthName} 1, ${year}`;
50+
}
51+
}

src/assets/i18n/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2834,6 +2834,7 @@
28342834
},
28352835
"summary": {
28362836
"title": "Summary",
2837+
"lastUpdated": "Last Updated",
28372838
"totalUsersByDepartment": "Total Users by Department",
28382839
"publicPrivateProjects": "Public vs Private Projects",
28392840
"publicEmbargoedRegistrations": "Public vs Embargoed Registrations",

0 commit comments

Comments
 (0)