Skip to content

Commit

Permalink
Merge branch 'main' into chore/GEO-1145-announcement-diagrams
Browse files Browse the repository at this point in the history
  • Loading branch information
jer3k authored Sep 25, 2024
2 parents 2a4e18b + 2b28a79 commit 2aa1675
Show file tree
Hide file tree
Showing 15 changed files with 301 additions and 28 deletions.
6 changes: 4 additions & 2 deletions admin-frontend/src/components/DashboardPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
></NumSubmissionsInYear>
</v-col>
<v-col>
<NumEmployerLogins></NumEmployerLogins>
<NumEmployerLogons ref="numEmployerLogons"></NumEmployerLogons>
</v-col>
</v-row>
</v-col>
Expand Down Expand Up @@ -79,7 +79,7 @@
import RecentlySubmittedReports from './dashboard/RecentlySubmittedReports.vue';
import RecentlyViewedReports from './dashboard/RecentlyViewedReports.vue';
import NumSubmissionsInYear from './dashboard/NumSubmissionsInYear.vue';
import NumEmployerLogins from './dashboard/NumEmployerLogins.vue';
import NumEmployerLogons from './dashboard/NumEmployerLogons.vue';
import PublicAnnouncements from './dashboard/PublicAnnouncements.vue';
import ToolTip from './ToolTip.vue';
import { ref, onMounted } from 'vue';
Expand All @@ -92,6 +92,7 @@ const recentlySubmittedReports = ref<typeof RecentlySubmittedReports>();
const recentlyViewedReports = ref<typeof RecentlyViewedReports>();
const numSubmissionsInYear = ref<typeof NumSubmissionsInYear>();
const publicAnnouncements = ref<typeof PublicAnnouncements>();
const numEmployerLogons = ref<typeof NumEmployerLogons>();
onMounted(() => {
//Periodically refresh the widgets
Expand All @@ -105,5 +106,6 @@ async function refresh() {
await recentlyViewedReports.value?.refresh();
await numSubmissionsInYear.value?.refresh();
await publicAnnouncements.value?.refresh();
await numEmployerLogons.value?.refresh();
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createVuetify } from 'vuetify';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';
import DashboardPage from '../DashboardPage.vue';
import NumEmployerLogins from '../dashboard/NumEmployerLogins.vue';
import NumEmployerLogins from '../dashboard/NumEmployerLogons.vue';

// Mock the ResizeObserver
const ResizeObserverMock = vi.fn(() => ({
Expand Down
24 changes: 0 additions & 24 deletions admin-frontend/src/components/dashboard/NumEmployerLogins.vue

This file was deleted.

75 changes: 75 additions & 0 deletions admin-frontend/src/components/dashboard/NumEmployerLogons.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<template>
<v-card class="ptap-widget">
<v-card-text class="h-100 d-flex flex-column">
<div class="widget-header flex-grow-0 flex-shrink-0">
Total number of employers who have logged on to date
</div>
<div
class="d-flex flex-column justify-center align-center text-primary flex-grow-1 flex-shrink-0"
>
<v-skeleton-loader v-if="isLoading" type="avatar"></v-skeleton-loader>
<div v-if="!isLoading">
<span v-if="hasError">
<v-tooltip text="Unable to load the data">
<template #activator="{ props }">
<v-icon
icon="mdi-alert"
size="x-large"
color="grey"
v-bind="props"
@click="refresh"
></v-icon>
</template>
</v-tooltip>
</span>
<span v-if="!hasError" class="widget-value">{{
numEmployersWhoHaveLoggedOn
}}</span>
</div>
</div>
</v-card-text>
</v-card>
</template>

<script setup lang="ts">
import ApiService from '../../services/apiService';
import { ref, onMounted } from 'vue';
import { EmployerMetrics } from '../../types/employers';
onMounted(() => {
refresh();
});
const numEmployersWhoHaveLoggedOn = ref<number | null>();
const hasError = ref<boolean>(false);
const isLoading = ref<boolean>(false);
async function refresh() {
hasError.value = false;
isLoading.value = true;
try {
const employerMetrics: EmployerMetrics =
await ApiService.getEmployerMetrics();
numEmployersWhoHaveLoggedOn.value =
employerMetrics?.num_employers_logged_on_to_date;
} catch (e) {
hasError.value = true;
} finally {
isLoading.value = false;
}
}
defineExpose({
refresh,
});
</script>

<style lang="scss">
.widget-header {
font-size: 1.2em;
}
.widget-value {
font-size: 6em;
font-weight: bold;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createTestingPinia } from '@pinia/testing';
import { render, screen, waitFor } from '@testing-library/vue';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createVuetify } from 'vuetify';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';
import { EmployerMetrics } from '../../../types/employers';
import NumEmployerLogons from '../NumEmployerLogons.vue';

global.ResizeObserver = require('resize-observer-polyfill');
const pinia = createTestingPinia();
const vuetify = createVuetify({ components, directives });

const mockEmployerMetrics: EmployerMetrics = {
num_employers_logged_on_to_date: 6,
};
const mockGetEmployerMetrics = vi.fn().mockResolvedValue(mockEmployerMetrics);

vi.mock('../../../services/apiService', () => ({
default: {
getEmployerMetrics: (...args) => {
return mockGetEmployerMetrics(...args);
},
},
}));

const wrappedRender = () => {
return render(NumEmployerLogons, {
global: {
plugins: [pinia, vuetify],
},
});
};

describe('NumEmployerLogons', () => {
afterEach(() => {
vi.clearAllMocks();
});

it('displays the number of employers who have logged on to date', async () => {
await wrappedRender();
expect(mockGetEmployerMetrics).toHaveBeenCalled();

await waitFor(() => {
expect(
screen.getByText(
`${mockEmployerMetrics.num_employers_logged_on_to_date}`,
{
exact: true,
},
),
).toBeInTheDocument();
});
});
});
25 changes: 24 additions & 1 deletion admin-frontend/src/services/__tests__/apiService.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AxiosError } from 'axios';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AnnouncementStatus } from '../../types/announcements';
import { EmployerMetrics } from '../../types/employers';
import { ReportMetrics } from '../../types/reports';
import ApiService from '../apiService';

Expand Down Expand Up @@ -652,9 +653,31 @@ describe('ApiService', () => {
});
});

describe('getEmployerMetrics', () => {
describe('when the data are successfully retrieved from the backend', () => {
it('returns an object with the expected metrics', async () => {
const mockBackendResponse = {
num_employers_logged_on_to_date: 8,
};
const mockAxiosResponse = {
data: mockBackendResponse,
};
vi.spyOn(ApiService.apiAxios, 'get').mockResolvedValueOnce(
mockAxiosResponse,
);

const resp: EmployerMetrics = await ApiService.getEmployerMetrics();
expect(resp).toEqual(mockBackendResponse);
});
});
});

describe('getAnnouncementsMetrics', () => {
it('returns an announcement metrics', async () => {
const mockBackendResponse = { draft: { count: 1 }, published: { count: 2 } };
const mockBackendResponse = {
draft: { count: 1 },
published: { count: 2 },
};
const mockAxiosResponse = {
data: mockBackendResponse,
};
Expand Down
14 changes: 14 additions & 0 deletions admin-frontend/src/services/apiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
AnnouncementSortType,
IAnnouncementSearchResult,
} from '../types/announcements';
import { EmployerMetrics } from '../types/employers';
import { IReportSearchResult, ReportMetrics } from '../types/reports';
import { ApiRoutes, POWERBI_RESOURCE } from '../utils/constant';
import AuthService from './authService';
Expand Down Expand Up @@ -276,6 +277,19 @@ export default {
}
},

async getEmployerMetrics(): Promise<EmployerMetrics> {
try {
const resp = await apiAxios.get(ApiRoutes.EMPLOYER_METRICS);
if (resp?.data) {
return resp.data;
}
throw new Error('Unable to fetch employer metrics');
} catch (e) {
console.log(`Failed to get employer metrics from API - ${e}`);
throw e;
}
},

async getAnnouncements(
offset: number = 0,
limit: number = 20,
Expand Down
3 changes: 3 additions & 0 deletions admin-frontend/src/types/employers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type EmployerMetrics = {
num_employers_logged_on_to_date: number;
};
1 change: 1 addition & 0 deletions admin-frontend/src/utils/constant.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const ApiRoutes = Object.freeze({
CLAMAV_SCAN: `${clamavBaseRoot}/`,
RESOURCES: `${baseRoot}/v1/resources`,
REPORT_METRICS: `${baseRoot}/v1/dashboard/reports-metrics`,
EMPLOYER_METRICS: `${baseRoot}/v1/dashboard/employer-metrics`,
});

export const PAGE_TITLES = Object.freeze({
Expand Down
51 changes: 51 additions & 0 deletions backend/src/v1/routes/dashboard/employer-metrics-routes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import express, { Application } from 'express';
import request from 'supertest';
import router from './employer-metrics-routes';

let app: Application;
const getEmployerMetricsMock = jest.fn();
jest.mock('../../services/employer-service', () => ({
employerService: {
getEmployerMetrics: (...args) => getEmployerMetricsMock(...args),
},
}));
describe('employer-metrics-routes', () => {
beforeEach(() => {
jest.clearAllMocks();
app = express();
app.use('/dashboard', router);
});

describe('GET /employer-metrics', () => {
describe('200', () => {
it('should return the announcement metrics', async () => {
// Arrange
getEmployerMetricsMock.mockResolvedValueOnce({});

// Act
const response = await request(app).get('/dashboard/employer-metrics');

// Assert
expect(getEmployerMetricsMock).toHaveBeenCalledTimes(1);
expect(response.status).toBe(200);
});
});
describe('500', () => {
it('should return 500 if an error occurs', async () => {
// Arrange
getEmployerMetricsMock.mockRejectedValueOnce(
new Error('An error occurred'),
);

// Act
const response = await request(app).get('/dashboard/employer-metrics');

// Assert
expect(response.status).toBe(500);
expect(response.body).toEqual({
error: 'An error occurred while fetching the employer metrics',
});
});
});
});
});
22 changes: 22 additions & 0 deletions backend/src/v1/routes/dashboard/employer-metrics-routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Router } from 'express';
import { logger } from '../../../logger';
import { employerService } from '../../services/employer-service';

const router = Router();

/**
* Get employer metrics
*/
router.get('/employer-metrics', async (req, res) => {
try {
const metrics = await employerService.getEmployerMetrics();
res.json(metrics);
} catch (error) {
logger.error(error);
res.status(500).send({
error: 'An error occurred while fetching the employer metrics',
});
}
});

export default router;
2 changes: 2 additions & 0 deletions backend/src/v1/routes/dashboard/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Router } from 'express';
import announcementMetricsRouter from './announcement-metrics-routes';
import employerMetricsRouter from './employer-metrics-routes';
import reportsMetricsRouter from './report-metrics-routes';

const router = Router();

router.use(announcementMetricsRouter);
router.use(reportsMetricsRouter);
router.use(employerMetricsRouter);

export default router;
30 changes: 30 additions & 0 deletions backend/src/v1/services/employer-service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { employerService } from '../services/employer-service';
import { EmployerMetrics } from '../types/employers';

const mockCountPayTransparencyCompanies = jest.fn();

jest.mock('../prisma/prisma-client-readonly-replica', () => ({
__esModule: true,
...jest.requireActual('../prisma/prisma-client-readonly-replica'),
default: {
pay_transparency_company: {
count: () => mockCountPayTransparencyCompanies(),
},
},
}));

describe('employer-service', () => {
describe('getEmployerMetrics', () => {
it('delegates request to the database', async () => {
const numCompaniesLoggedOnToDate = 16;
mockCountPayTransparencyCompanies.mockResolvedValue(
numCompaniesLoggedOnToDate,
);
const employerMetrics: EmployerMetrics =
await employerService.getEmployerMetrics();
expect(employerMetrics.num_employers_logged_on_to_date).toBe(
numCompaniesLoggedOnToDate,
);
});
});
});
Loading

0 comments on commit 2aa1675

Please sign in to comment.