Files
e2e 6e43ee3df9
Some checks failed
Main / Setup and Test (push) Has been cancelled
Main / Build Website (push) Has been cancelled
chore: heygermany customer project
2026-07-11 10:40:10 +02:00

301 lines
12 KiB
TypeScript

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>
);
}