303 lines
12 KiB
TypeScript
303 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import { Container, Title, Text, TextInput, ActionIcon, Group, Button, Badge, Select } from '@pikku/mantine/core';
|
|
import { DataTable, type DataTableSortStatus } from 'mantine-datatable';
|
|
import { useDebouncedValue } from '@mantine/hooks';
|
|
import { IconSearch, IconX } from '@tabler/icons-react';
|
|
import sortBy from 'lodash/sortBy';
|
|
import dayjs from 'dayjs';
|
|
import { useGetBackOfficeCandidates } from '@/hooks/backoffice/useGetBackOfficeCandidates';
|
|
import { useI18n } from '@/context/i18n-provider';
|
|
import { useEffect, useState } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { pikku } from '@/pikku/http';
|
|
import { CountriesMap } from '@heygermany/sdk';
|
|
import { CountrySearch } from '@/components/jobs/form/CountrySearch';
|
|
import { LanguageLevelSelectComponent, NurseRecognitionGermanySelectComponent, ApplicantStatusSelectComponent } from '@/components/backoffice/DocumentFieldFactory';
|
|
|
|
export default function BackOfficeCandidates() {
|
|
const { isLoading, data: candidates } = useGetBackOfficeCandidates()
|
|
const t = useI18n();
|
|
const notAvailable = t('common.na');
|
|
|
|
// Fetch countries map
|
|
const { data: countriesMap = {} } = useQuery({
|
|
queryKey: ['countries', t.locale],
|
|
queryFn: async (): Promise<CountriesMap> => {
|
|
return await pikku().get('/countries', { locale: t.locale })
|
|
},
|
|
staleTime: 60 * 60 * 1000, // 1 hour
|
|
});
|
|
|
|
// Filter states
|
|
const [nameQuery, setNameQuery] = useState('');
|
|
const [debouncedNameQuery] = useDebouncedValue(nameQuery, 200);
|
|
const [countryFilter, setCountryFilter] = useState('');
|
|
const [germanLevelFilter, setGermanLevelFilter] = useState<string | null>(null);
|
|
const [recognitionStatusFilter, setRecognitionStatusFilter] = useState<string | null>(null);
|
|
const [statusFilter, setStatusFilter] = useState<string | null>(null);
|
|
const [reuploadedAfterInvalidFilter, setReuploadedAfterInvalidFilter] = useState<string | null>(null);
|
|
|
|
// Sort state
|
|
const [sortStatus, setSortStatus] = useState<DataTableSortStatus<any>>({
|
|
columnAccessor: 'createdAt',
|
|
direction: 'desc',
|
|
});
|
|
|
|
// Records state
|
|
const [records, setRecords] = useState<any[]>([]);
|
|
|
|
const hasReuploadedAfterInvalid = (candidate: any) => {
|
|
if (!candidate.reuploadedAt || !candidate.invalidatedAt) {
|
|
return false;
|
|
}
|
|
|
|
return new Date(candidate.reuploadedAt).getTime() > new Date(candidate.invalidatedAt).getTime();
|
|
};
|
|
|
|
// Apply filters
|
|
useEffect(() => {
|
|
if (!candidates?.data) {
|
|
setRecords([]);
|
|
return;
|
|
}
|
|
|
|
const filtered = candidates.data.filter((candidate: any) => {
|
|
// Name filter
|
|
if (debouncedNameQuery !== '' &&
|
|
!candidate.name?.toLowerCase().includes(debouncedNameQuery.trim().toLowerCase())) {
|
|
return false;
|
|
}
|
|
|
|
// Country filter
|
|
if (countryFilter && candidate.countryEducation !== countryFilter) {
|
|
return false;
|
|
}
|
|
|
|
// German level filter
|
|
if (germanLevelFilter && candidate.languageSelfAssessmentLevel !== germanLevelFilter) {
|
|
return false;
|
|
}
|
|
|
|
// Recognition status filter
|
|
if (recognitionStatusFilter && candidate.qualificationStatusGermany !== recognitionStatusFilter) {
|
|
return false;
|
|
}
|
|
|
|
// Status filter
|
|
if (statusFilter && candidate.status !== statusFilter) {
|
|
return false;
|
|
}
|
|
|
|
if (reuploadedAfterInvalidFilter === 'yes' && !hasReuploadedAfterInvalid(candidate)) {
|
|
return false;
|
|
}
|
|
|
|
if (reuploadedAfterInvalidFilter === 'no' && hasReuploadedAfterInvalid(candidate)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
setRecords(filtered);
|
|
}, [candidates?.data, debouncedNameQuery, countryFilter, germanLevelFilter, recognitionStatusFilter, statusFilter, reuploadedAfterInvalidFilter]);
|
|
|
|
// Apply sorting
|
|
useEffect(() => {
|
|
if (records.length === 0) return;
|
|
|
|
let sortedData = [...records];
|
|
|
|
if (sortStatus.columnAccessor === 'createdAt') {
|
|
sortedData = sortBy(sortedData, (item) => item.createdAt ? new Date(item.createdAt).getTime() : 0);
|
|
} else if (sortStatus.columnAccessor === 'fullName') {
|
|
sortedData = sortBy(sortedData, (item) => item.name || '');
|
|
} else if (sortStatus.columnAccessor === 'countryEducation') {
|
|
sortedData = sortBy(sortedData, (item) => item.countryEducation || '');
|
|
} else if (sortStatus.columnAccessor === 'germanLevel') {
|
|
sortedData = sortBy(sortedData, (item) => item.languageSelfAssessmentLevel || '');
|
|
} else if (sortStatus.columnAccessor === 'recognitionStatus') {
|
|
sortedData = sortBy(sortedData, (item) => item.qualificationStatusGermany || '');
|
|
} else if (sortStatus.columnAccessor === 'status') {
|
|
sortedData = sortBy(sortedData, (item) => item.status || '');
|
|
}
|
|
|
|
setRecords(sortStatus.direction === 'desc' ? sortedData.reverse() : sortedData);
|
|
}, [sortStatus]);
|
|
|
|
const clearFilters = () => {
|
|
setNameQuery('');
|
|
setCountryFilter('');
|
|
setGermanLevelFilter(null);
|
|
setRecognitionStatusFilter(null);
|
|
setStatusFilter(null);
|
|
setReuploadedAfterInvalidFilter(null);
|
|
};
|
|
|
|
const hasActiveFilters =
|
|
nameQuery !== '' ||
|
|
countryFilter !== '' ||
|
|
germanLevelFilter !== null ||
|
|
recognitionStatusFilter !== null ||
|
|
statusFilter !== null ||
|
|
reuploadedAfterInvalidFilter !== null;
|
|
|
|
return (
|
|
<Container size="xl">
|
|
<Title order={1}>
|
|
{t('backoffice.candidates.title')}
|
|
</Title>
|
|
<Group justify="space-between" mt="sm" mb="lg">
|
|
<Text size="lg" c="dimmed">
|
|
{t('backoffice.candidates.description')}
|
|
</Text>
|
|
<Group gap="md">
|
|
<Text size="sm" c="dimmed" fw={500}>
|
|
{hasActiveFilters
|
|
? t('backoffice.candidates.count.filtered', {
|
|
filtered: records.length,
|
|
total: candidates?.total || 0,
|
|
})
|
|
: t('backoffice.candidates.count.total', {
|
|
total: candidates?.total || 0,
|
|
})}
|
|
</Text>
|
|
{hasActiveFilters && (
|
|
<Button variant="light" onClick={clearFilters}>
|
|
{t('backoffice.candidates.clearfilters')}
|
|
</Button>
|
|
)}
|
|
</Group>
|
|
</Group>
|
|
|
|
<DataTable
|
|
withTableBorder
|
|
borderRadius="md"
|
|
striped
|
|
highlightOnHover
|
|
height='calc(100vh - 220px)'
|
|
minHeight={200}
|
|
fetching={isLoading}
|
|
records={records}
|
|
sortStatus={sortStatus}
|
|
onSortStatusChange={setSortStatus}
|
|
onRowClick={({ record }) =>
|
|
window.open(`/backoffice/candidate/${record.candidateId}`, '_blank', 'noopener,noreferrer')
|
|
}
|
|
columns={[
|
|
{
|
|
accessor: 'fullName',
|
|
title: t('backoffice.candidates.columns.fullname'),
|
|
sortable: true,
|
|
render: (candidate) => candidate.name,
|
|
filter: (
|
|
<TextInput
|
|
label={t('backoffice.candidates.filters.name.label')}
|
|
description={t('backoffice.candidates.filters.name.description')}
|
|
placeholder={t('backoffice.candidates.filters.name.placeholder')}
|
|
leftSection={<IconSearch size={16} />}
|
|
rightSection={
|
|
<ActionIcon size="sm" variant="transparent" c="dimmed" onClick={() => setNameQuery('')}>
|
|
<IconX size={14} />
|
|
</ActionIcon>
|
|
}
|
|
value={nameQuery}
|
|
onChange={(e) => setNameQuery(e.currentTarget.value)}
|
|
/>
|
|
),
|
|
filtering: nameQuery !== '',
|
|
},
|
|
{
|
|
accessor: 'countryEducation',
|
|
title: t('backoffice.candidates.columns.countryofeducation'),
|
|
sortable: true,
|
|
render: (candidate) => candidate.countryEducation ? (countriesMap[candidate.countryEducation] || candidate.countryEducation) : notAvailable,
|
|
filter: (
|
|
<CountrySearch
|
|
label={t('backoffice.candidates.filters.country.label')}
|
|
placeholder={t('backoffice.candidates.filters.country.placeholder')}
|
|
value={countryFilter}
|
|
onChange={setCountryFilter}
|
|
/>
|
|
),
|
|
filtering: countryFilter !== '',
|
|
},
|
|
{
|
|
accessor: 'germanLevel',
|
|
title: t('backoffice.candidates.columns.germanlevel'),
|
|
sortable: true,
|
|
render: (candidate) => candidate.languageSelfAssessmentLevel ? t(`enums.languagelevel.${candidate.languageSelfAssessmentLevel.toLowerCase()}` as any) : notAvailable,
|
|
filter: (
|
|
<LanguageLevelSelectComponent
|
|
label={t('backoffice.candidates.filters.germanlevel.label')}
|
|
placeholder={t('backoffice.candidates.filters.germanlevel.placeholder')}
|
|
value={germanLevelFilter}
|
|
onChange={setGermanLevelFilter}
|
|
/>
|
|
),
|
|
filtering: germanLevelFilter !== null,
|
|
},
|
|
{
|
|
accessor: 'recognitionStatus',
|
|
title: t('backoffice.candidates.columns.recognitionstatus'),
|
|
sortable: true,
|
|
render: (candidate) => candidate.qualificationStatusGermany ? t(`enums.nurserecognitiongermany.${candidate.qualificationStatusGermany.toLowerCase()}` as any) : notAvailable,
|
|
filter: (
|
|
<NurseRecognitionGermanySelectComponent
|
|
label={t('backoffice.candidates.filters.recognitionstatus.label')}
|
|
placeholder={t('backoffice.candidates.filters.recognitionstatus.placeholder')}
|
|
value={recognitionStatusFilter}
|
|
onChange={setRecognitionStatusFilter}
|
|
/>
|
|
),
|
|
filtering: recognitionStatusFilter !== null,
|
|
},
|
|
{
|
|
accessor: 'createdAt',
|
|
title: t('backoffice.candidates.columns.creationdate'),
|
|
sortable: true,
|
|
render: (candidate) => dayjs(candidate.createdAt).format('YYYY-MM-DD')
|
|
},
|
|
{
|
|
accessor: 'status',
|
|
title: t('backoffice.candidates.columns.status'),
|
|
sortable: true,
|
|
render: (candidate) => candidate.status ? t(`enums.applicantstatus.${candidate.status.toLowerCase()}` as any) : notAvailable,
|
|
filter: (
|
|
<ApplicantStatusSelectComponent
|
|
label={t('backoffice.candidates.filters.status.label')}
|
|
placeholder={t('backoffice.candidates.filters.status.placeholder')}
|
|
value={statusFilter}
|
|
onChange={setStatusFilter}
|
|
/>
|
|
),
|
|
filtering: statusFilter !== null,
|
|
},
|
|
{
|
|
accessor: 'reuploadedAfterInvalid',
|
|
title: t('backoffice.candidates.columns.reuploaded'),
|
|
render: (candidate) => hasReuploadedAfterInvalid(candidate)
|
|
? <Badge color="yellow" variant="light">{t('backoffice.candidates.reuploaded.badge')}</Badge>
|
|
: <Text size="sm" c="dimmed">{t('common.no')}</Text>,
|
|
filter: (
|
|
<Select
|
|
label={t('backoffice.candidates.filters.reuploaded.label')}
|
|
placeholder={t('backoffice.candidates.filters.reuploaded.placeholder')}
|
|
value={reuploadedAfterInvalidFilter}
|
|
onChange={setReuploadedAfterInvalidFilter}
|
|
data={[
|
|
{ value: 'yes', label: t('common.yes') },
|
|
{ value: 'no', label: t('common.no') },
|
|
]}
|
|
clearable
|
|
/>
|
|
),
|
|
filtering: reuploadedAfterInvalidFilter !== null,
|
|
}
|
|
]}
|
|
/>
|
|
</Container>
|
|
);
|
|
}
|