chore: heygermany customer project
This commit is contained in:
302
apps/website/views/[lang]/(app)/backoffice/candidates/page.tsx
Normal file
302
apps/website/views/[lang]/(app)/backoffice/candidates/page.tsx
Normal file
@@ -0,0 +1,302 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user