Skip to content

Commit

Permalink
feat: GEO-1159 - admin frontend Employer Search page (#797)
Browse files Browse the repository at this point in the history
  • Loading branch information
banders authored Oct 4, 2024
1 parent 70c7d73 commit c84ea7e
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 17 deletions.
229 changes: 229 additions & 0 deletions admin-frontend/src/components/EmployersPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<template>
<v-row dense class="mt-0 w-100 mb-4">
<v-col sm="12" md="7" lg="6" xl="4" class="d-flex flex-column justify-end">
<h3 class="mb-2">Search employer</h3>
<v-text-field
v-model="searchText"
prepend-inner-icon="mdi-magnify"
density="compact"
label="Search by employer name"
variant="solo"
hide-details
:single-line="true"
class="flex-shrink-1 flex-grow-0"
@keyup.enter="search()"
>
</v-text-field>
</v-col>

<v-col sm="7" md="5" lg="3" xl="2" class="d-flex flex-column justify-end">
<h5 class="mt-4">
Calendar Year(s)
<ToolTip
:text="'Select a calendar year to view employers by the first date they logged in.'"
max-width="300px"
></ToolTip>
</h5>
<v-select
v-model="selectedYears"
:items="yearOptions"
:persistent-placeholder="true"
placeholder="All"
label="Calendar Year(s)"
:single-line="true"
multiple
class="calendar-year flex-shrink-1 flex-grow-0"
variant="solo"
density="compact"
>
<template #item="{ props, item }">
<v-list-item v-bind="props" :title="`${item.raw}`">
<template #append="{ isActive }">
<v-list-item-action start>
<v-checkbox-btn :model-value="isActive"></v-checkbox-btn>
</v-list-item-action>
</template>
</v-list-item>
</template>
<template #selection="{ item, index }">
<v-chip v-if="index < maxSelectedYearShown">
<span>{{ item.raw }}</span>
</v-chip>
<span
v-if="index === maxSelectedYearShown"
class="text-grey text-caption align-self-center"
>
(+{{ selectedYears.length - maxSelectedYearShown }}
more)
</span>
</template>
</v-select>
</v-col>
<v-col sm="4" md="12" lg="3" xl="2" class="d-flex flex-column justify-end">
<!-- on screen size 'md', right-align the buttons, otherwise left-align them -->
<div
class="d-flex"
:class="
displayBreakpoint.name.value.valueOf() == 'md'
? 'justify-end'
: 'justify-start'
"
>
<v-btn
class="btn-primary me-2"
:loading="isSearching"
:disabled="isSearching"
@click="search()"
>
Search
</v-btn>
<v-btn class="btn-secondary" :disabled="!isDirty" @click="reset()">
Reset
</v-btn>
</div>
</v-col>
</v-row>

<v-row v-if="hasSearched" dense class="w-100">
<v-col>
<v-data-table-server
v-model:items-per-page="pageSize"
:headers="headers"
:items="searchResults"
:items-length="totalNum"
:loading="isSearching"
:items-per-page-options="pageSizeOptions"
search=""
:no-data-text="
hasSearched ? 'No reports matched the search criteria' : ''
"
@update:options="updateSearch"
>
<template #item.create_date="{ item }">
{{ formatDate(item.create_date) }}
</template>
</v-data-table-server>
</v-col>
</v-row>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';
import { formatDate } from '../utils/date';
import {
Employer,
EmployerFilterType,
EmployerSortType,
EmployerKeyEnum,
} from '../types/employers';
import { useDisplay } from 'vuetify';
import { NotificationService } from '../services/notificationService';
import ToolTip from './ToolTip.vue';
import ApiService from '../services/apiService';
const displayBreakpoint = useDisplay();
const firstSearchableYear = 2024;
const currentYear = new Date().getFullYear();
//make a list of years from 'firstSearchableYear' to 'currentYear'
const yearOptions = new Array(currentYear - firstSearchableYear + 1)
.fill(0)
.map((d, i) => i + firstSearchableYear);
const searchText = ref<string | undefined>(undefined);
const selectedYears = ref<number[]>([]);
const maxSelectedYearShown = 2;
const pageSizeOptions = [10, 25, 50];
const pageSize = ref<number>(pageSizeOptions[1]);
const searchResults = ref<Employer[] | undefined>(undefined);
const totalNum = ref<number>(0);
const isSearching = ref<boolean>(false);
const hasSearched = ref<boolean>(false);
const isDirty = computed(() => {
return hasSearched.value || searchText.value || selectedYears.value?.length;
});
const headers = ref<any>([
{
title: 'Company Name',
align: 'start',
sortable: true,
key: 'company_name',
},
{
title: 'Date of First Log On',
align: 'start',
sortable: true,
key: 'create_date',
},
]);
function reset() {
searchText.value = undefined;
selectedYears.value = [];
pageSize.value = pageSizeOptions[1];
searchResults.value = undefined;
totalNum.value = 0;
hasSearched.value = false;
}
function buildSearchFilters(): EmployerFilterType {
const filters: EmployerFilterType = [];
if (searchText.value) {
filters.push({
key: EmployerKeyEnum.Name,
value: searchText.value,
operation: 'like',
});
}
if (selectedYears.value?.length) {
filters.push({
key: EmployerKeyEnum.Year,
value: selectedYears.value,
operation: 'in',
});
}
return filters;
}
function buildSort(sortOptions): EmployerSortType {
const sort: EmployerSortType = sortOptions?.map((d) => {
return { field: d.key, order: d.order };
});
return sort;
}
async function search(options?) {
isSearching.value = true;
try {
const offset = options ? (options.page - 1) * options.itemsPerPage : 0;
const limit = pageSize.value;
const filter: EmployerFilterType = buildSearchFilters();
const sort: EmployerSortType = buildSort(options?.sortBy);
const resp = await ApiService.getEmployers(offset, limit, filter, sort);
searchResults.value = resp?.employers;
totalNum.value = resp?.total;
} catch (e) {
console.log(e);
NotificationService.pushNotificationError('Unable to search employers');
} finally {
hasSearched.value = true;
isSearching.value = false;
}
}
function updateSearch(options: any) {
if (!hasSearched.value) {
return;
}
search(options);
}
</script>
<style>
.v-select > .v-input__details {
display: none;
}
.v-input.calendar-year label {
background-color: red;
}
</style>
33 changes: 22 additions & 11 deletions admin-frontend/src/components/SideBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,65 +22,76 @@
alt="B.C. Government Logo"
class="mb-8"
/>
<div class="d-flex justify-center mb-8 title" v-if="isExpanded">
<div v-if="isExpanded" class="d-flex justify-center mb-8 text-h6">
Pay Transparency Admin Portal
</div>
</div>
<v-list-item
v-if="auth.doesUserHaveRole(USER_ROLE_NAME)"
link
to="dashboard"
title="Dashboard"
:class="{ active: activeRoute == 'dashboard' }"
v-if="auth.doesUserHaveRole(USER_ROLE_NAME)"
>
<template v-slot:prepend>
<template #prepend>
<v-icon icon="mdi-home"></v-icon>
</template>
</v-list-item>
<v-list-item
v-if="auth.doesUserHaveRole(USER_ROLE_NAME)"
link
to="reports"
title="Search Reports"
:class="{ active: activeRoute == 'reports' }"
v-if="auth.doesUserHaveRole(USER_ROLE_NAME)"
>
<template v-slot:prepend>
<template #prepend>
<v-icon icon="mdi-magnify"></v-icon>
</template>
</v-list-item>
<v-list-item
v-if="auth.doesUserHaveRole(USER_ROLE_NAME)"
link
to="analytics"
title="Analytics"
:class="{ active: activeRoute == 'analytics' }"
v-if="auth.doesUserHaveRole(USER_ROLE_NAME)"
>
<template v-slot:prepend>
<template #prepend>
<v-icon icon="mdi-chart-bar"></v-icon>
</template>
</v-list-item>
<v-list-item
v-if="auth.doesUserHaveRole(ADMIN_ROLE_NAME)"
link
to="user-management"
title="User Management"
:class="{ active: activeRoute == 'user-management' }"
v-if="auth.doesUserHaveRole(ADMIN_ROLE_NAME)"
>
<template v-slot:prepend>
<template #prepend>
<v-icon icon="mdi-account-multiple"></v-icon>
</template>
</v-list-item>
<v-list-item
v-if="auth.doesUserHaveRole(USER_ROLE_NAME)"
link
to="announcements"
title="Announcements"
:class="{ active: activeRoute == 'announcements' }"
v-if="auth.doesUserHaveRole(USER_ROLE_NAME)"
>
<template v-slot:prepend>
<template #prepend>
<v-icon icon="mdi-bullhorn"></v-icon>
</template>
</v-list-item>
<v-list-item
v-if="auth.doesUserHaveRole(USER_ROLE_NAME)"
link
to="employers"
title="Employer Search"
:class="{ active: activeRoute == 'employers' }"
>
<template #prepend>
<v-icon icon="mdi-office-building"></v-icon>
</template>
</v-list-item>
</v-navigation-drawer>
</template>

Expand Down
5 changes: 3 additions & 2 deletions admin-frontend/src/components/ToolTip.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<v-tooltip :id="id" :text="text ? text : ''" :max-width="maxWidth">
<v-tooltip v-if="text" :id="id" :text="text" :max-width="maxWidth">
<template #activator="{ props }">
<v-icon
v-bind="props"
Expand All @@ -9,7 +9,7 @@
class="ml-1"
tabindex="0"
role="tooltip"
:aria-labeledby="ariaLabel"
:aria-labeledby="ariaLabel ? ariaLabel : text"
/>
</template>
</v-tooltip>
Expand All @@ -24,6 +24,7 @@
* max-width="300px">
* </ToolTip>
*/
defineProps<{
id?: string | undefined;
text?: string | undefined;
Expand Down
53 changes: 53 additions & 0 deletions admin-frontend/src/components/__tests__/EmployersPage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createTestingPinia } from '@pinia/testing';
import { fireEvent, render } from '@testing-library/vue';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createVuetify } from 'vuetify';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';
import EmployersPage from '../EmployersPage.vue';

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

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

describe('EmployersPage', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('required form elements are present', async () => {
const { getByRole, getByLabelText } = await wrappedRender();
expect(getByRole('button', { name: 'Search' })).toBeInTheDocument();
expect(getByRole('button', { name: 'Reset' })).toBeInTheDocument();
expect(getByLabelText('Calendar Year(s)')).toBeInTheDocument();
expect(getByLabelText('Search by employer name')).toBeInTheDocument();
});
describe('search', () => {
it('searches and displays the results', async () => {
const { getByRole } = await wrappedRender();
const searchBtn = getByRole('button', { name: 'Search' });
await fireEvent.click(searchBtn);
});
});
describe('reset', () => {
it('resets the search controls', async () => {
const { getByRole, getByLabelText } = await wrappedRender();
const mockSearchText = 'mock employer name';
const employerName = getByLabelText('Search by employer name');
const calendarYears = getByLabelText('Calendar Year(s)');
const resetBtn = getByRole('button', { name: 'Reset' });
await fireEvent.update(employerName, mockSearchText);
await fireEvent.update(calendarYears, `${new Date().getFullYear()}`);
await fireEvent.click(resetBtn);
expect(employerName).toHaveValue('');
expect(calendarYears).toHaveValue('');
});
});
});
Loading

0 comments on commit c84ea7e

Please sign in to comment.