-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: GEO-1159 - admin frontend Employer Search page (#797)
- Loading branch information
Showing
8 changed files
with
323 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
53 changes: 53 additions & 0 deletions
53
admin-frontend/src/components/__tests__/EmployersPage.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(''); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.