chore: heygermany customer project
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Container, Title, Text, Paper, Stack, Loader, Alert, Grid, Group, Flex, Card, Button, Switch, ActionIcon } from '@pikku/mantine/core';
|
||||
import { IconExternalLink, IconEye } from '@tabler/icons-react';
|
||||
import { EnumSelect } from '@/components/common/EnumSelect';
|
||||
import dayjs from 'dayjs';
|
||||
import { useParams } from '@tanstack/react-router';
|
||||
import { useGetBackOfficeCandidate } from '@/hooks/backoffice/useGetBackOfficeCandidate';
|
||||
import { useUpdateBackOfficeCandidate } from '@/hooks/backoffice/useUpdateBackOfficeCandidate';
|
||||
import { useGetBackOfficeCandidateResult } from '@/hooks/backoffice/useGetBackOfficeCandidateResult';
|
||||
import { EducationVerificationCard } from '@/components/backoffice/EducationVerificationCard';
|
||||
import { LicenseVerificationCard } from '@/components/backoffice/LicenseVerificationCard';
|
||||
import { LanguageVerificationCard } from '@/components/backoffice/LanguageVerificationCard';
|
||||
import { ExperienceVerificationCard } from '@/components/backoffice/ExperienceVerificationCard';
|
||||
import type { DB } from '@heygermany/sdk';
|
||||
import {
|
||||
NurseRecognitionGermanyValues,
|
||||
NurseRecognitionHomeCountryValues,
|
||||
RecognitionLikelihoodValues,
|
||||
} from '@heygermany/sdk';
|
||||
import { CandidateResult } from '@/components/jobs/CandidateResult';
|
||||
import { QualificationAssessment } from '@/components/backoffice/QualificationAssessment';
|
||||
import { BooleanSelectComponent } from '@/components/backoffice/DocumentFieldFactory';
|
||||
import { useGetCandidateResults } from '@/hooks/useGetCandidateResult';
|
||||
import { ApplicantStatusSelect } from '@/components/backoffice/ApplicantStatusSelect';
|
||||
import { useGetCountries } from '@/hooks/useGetCountries';
|
||||
import { LifecycleNotificationsCard } from '@/components/backoffice/LifecycleNotificationsCard';
|
||||
import { asI18n } from '@pikku/react';
|
||||
import { useI18n } from '@/context/i18n-provider';
|
||||
|
||||
const CandidateResultPreview: React.FunctionComponent<{ candidateId: string }> = ({ candidateId }) => {
|
||||
const { data: candidateResults } = useGetCandidateResults(candidateId)
|
||||
if (!candidateResults) return null
|
||||
return <CandidateResult candidateResults={candidateResults} />
|
||||
}
|
||||
|
||||
export default function BackOfficeCandidateDetailPage() {
|
||||
const params = useParams({ strict: false });
|
||||
const candidateId = params.candidateId as string;
|
||||
const t = useI18n();
|
||||
|
||||
const { data: candidate, isLoading, error } = useGetBackOfficeCandidate(candidateId);
|
||||
const { data: candidateResult } = useGetBackOfficeCandidateResult(candidateId);
|
||||
const updateCandidateMutation = useUpdateBackOfficeCandidate(candidateId);
|
||||
const { getCountryNameFromAbbreviation } = useGetCountries();
|
||||
const [showResultsDrawer, setShowResultsDrawer] = useState(false);
|
||||
const [verbose, setVerbose] = useState(false);
|
||||
const [analysisIssuesDraft, setAnalysisIssuesDraft] = useState('');
|
||||
const notAvailable = t('common.na');
|
||||
const yesNo = (value: boolean | null | undefined) => {
|
||||
if (value === true) return t('common.yes');
|
||||
if (value === false) return t('common.no');
|
||||
return notAvailable;
|
||||
};
|
||||
|
||||
const currentAnalysisIssues = Array.isArray(candidate?.lifecycleNotifications?.analysis?.issues)
|
||||
? candidate.lifecycleNotifications.analysis.issues.filter((issue: any): issue is string => typeof issue === 'string')
|
||||
: [];
|
||||
const currentAnalysisIssuesDraft = currentAnalysisIssues.join('\n');
|
||||
|
||||
useEffect(() => {
|
||||
setAnalysisIssuesDraft(currentAnalysisIssuesDraft);
|
||||
}, [candidateId, currentAnalysisIssuesDraft]);
|
||||
|
||||
const saveAnalysisIssues = () => {
|
||||
const analysisIssues = analysisIssuesDraft
|
||||
.split('\n')
|
||||
.map((issue: string) => issue.trim())
|
||||
.filter((issue: string) => issue.length > 0)
|
||||
|
||||
updateCandidateMutation.mutate({ analysisIssues })
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Flex justify="center" align="center" style={{ minHeight: 'calc(100vh - 60px)' }}>
|
||||
<Loader size="xl" />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !candidate) {
|
||||
return (
|
||||
<Container size="xl">
|
||||
<Alert color="red" title={t('common.error')}>
|
||||
{t('backoffice.candidate.error.notfound')}
|
||||
</Alert>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex>
|
||||
<Container>
|
||||
<Stack>
|
||||
<Group justify="space-between" align="center" mb="lg">
|
||||
<Title order={1}>
|
||||
{asI18n(candidate.name)}
|
||||
</Title>
|
||||
<Group gap="sm">
|
||||
<ApplicantStatusSelect
|
||||
value={candidate.status}
|
||||
onChange={(status) => {
|
||||
updateCandidateMutation.mutate({ status: status! })
|
||||
}}
|
||||
isValid={candidateResult?.isValid ?? false}
|
||||
/>
|
||||
<Group gap="xs">
|
||||
<Text size="sm" fw={500}>{t('backoffice.candidate.verbose')}</Text>
|
||||
<Switch
|
||||
checked={verbose}
|
||||
onChange={(event) => setVerbose(event.currentTarget.checked)}
|
||||
/>
|
||||
</Group>
|
||||
{candidateResult && (
|
||||
<Text
|
||||
size="sm"
|
||||
c={candidateResult.isValid ? 'success' : 'error'}
|
||||
fw={500}
|
||||
>
|
||||
{candidateResult.isValid ? t('backoffice.candidate.result.valid') : t('backoffice.candidate.result.invalid')}
|
||||
</Text>
|
||||
)}
|
||||
<ActionIcon
|
||||
size="lg"
|
||||
variant="light"
|
||||
disabled={!candidateResult?.isValid}
|
||||
onClick={() => setShowResultsDrawer(!showResultsDrawer)}
|
||||
title={t('backoffice.candidate.actions.showresultspanel')}
|
||||
>
|
||||
<IconEye size={18} />
|
||||
</ActionIcon>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="filled"
|
||||
disabled={!candidateResult?.isValid}
|
||||
onClick={() => window.open(`/jobs/application?candidateId=${candidateId}`, '_blank')}
|
||||
leftSection={<IconExternalLink size={16} />}
|
||||
>
|
||||
{t('backoffice.candidate.actions.resultpage')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
disabled={!candidateResult?.isValid}
|
||||
onClick={() => window.open(`/company/candidate/${candidateId}`, '_blank')}
|
||||
leftSection={<IconExternalLink size={16} />}
|
||||
>
|
||||
{t('backoffice.candidate.actions.companyview')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* Qualification Result Summary */}
|
||||
<QualificationAssessment candidateResult={candidateResult} />
|
||||
|
||||
{/* Manual Assessment Section */}
|
||||
<Title order={3} mb="md">{t('backoffice.candidate.manualassessment')}</Title>
|
||||
<Card shadow="sm" padding="lg" radius="md" withBorder mb="md">
|
||||
<Grid gutter="md">
|
||||
<Grid.Col span={6}>
|
||||
<EnumSelect
|
||||
enumObject={NurseRecognitionHomeCountryValues}
|
||||
enumName="NurseRecognitionHomeCountry"
|
||||
label={t('backoffice.candidate.fields.statushomecountry')}
|
||||
placeholder={t('backoffice.candidate.fields.selectstatus')}
|
||||
value={candidate.qualificationStatusHomeCountry || null}
|
||||
onChange={(qualificationStatusHomeCountry) =>
|
||||
updateCandidateMutation.mutate({
|
||||
qualificationStatusHomeCountry: qualificationStatusHomeCountry! as DB.NurseRecognitionHomeCountry
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<EnumSelect
|
||||
enumObject={NurseRecognitionGermanyValues}
|
||||
enumName="NurseRecognitionGermany"
|
||||
label={t('backoffice.candidate.fields.statusgermany')}
|
||||
placeholder={t('backoffice.candidate.fields.selectstatus')}
|
||||
value={candidate.qualificationStatusGermany || null}
|
||||
onChange={(qualificationStatusGermany) =>
|
||||
updateCandidateMutation.mutate({
|
||||
qualificationStatusGermany: qualificationStatusGermany! as DB.NurseRecognitionGermany
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<BooleanSelectComponent
|
||||
label={t('backoffice.candidate.fields.licensemandatory')}
|
||||
value={candidate.licenseMandatory}
|
||||
onChange={(licenseMandatory) =>
|
||||
updateCandidateMutation.mutate({ licenseMandatory })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<BooleanSelectComponent
|
||||
label={t('backoffice.candidate.fields.durationrequirementsmet')}
|
||||
value={candidate.durationRequirementsMet}
|
||||
onChange={(durationRequirementsMet) =>
|
||||
updateCandidateMutation.mutate({ durationRequirementsMet })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<EnumSelect
|
||||
enumObject={RecognitionLikelihoodValues}
|
||||
enumName="RecognitionLikelihood"
|
||||
label={t('backoffice.candidate.fields.recognitionlikelihood')}
|
||||
placeholder={t('backoffice.candidate.fields.selectlikelihood')}
|
||||
value={candidate.recognitionLikelihood || null}
|
||||
onChange={(recognitionLikelihood) =>
|
||||
updateCandidateMutation.mutate({
|
||||
recognitionLikelihood: recognitionLikelihood! as DB.RecognitionLikelihood
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Card>
|
||||
<Title order={2} mb="md">{t('backoffice.candidate.basicinfo')}</Title>
|
||||
|
||||
<Card shadow="sm" padding="lg" radius="md" withBorder>
|
||||
<Stack gap="md">
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<Group gap="xs">
|
||||
<Text fw={500} size="sm">{t('backoffice.candidate.fields.email')}</Text>
|
||||
<Text size="sm">{candidate.email ? asI18n(candidate.email) : notAvailable}</Text>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Group gap="xs">
|
||||
<Text fw={500} size="sm">{t('backoffice.candidate.fields.currentresidence')}</Text>
|
||||
<Text size="sm">{asI18n(getCountryNameFromAbbreviation(candidate.currentResidence))}</Text>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Group gap="xs">
|
||||
<Text fw={500} size="sm">{t('backoffice.candidate.fields.countryofeducation')}</Text>
|
||||
<Text size="sm">{asI18n(getCountryNameFromAbbreviation(candidate.countryEducation))}</Text>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Group gap="xs">
|
||||
<Text fw={500} size="sm">{t('backoffice.candidate.fields.eupassport')}</Text>
|
||||
<Text size="sm">{yesNo(candidate.euPassport)}</Text>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Group gap="xs">
|
||||
<Text fw={500} size="sm">{t('backoffice.candidate.fields.dateofbirthage')}</Text>
|
||||
<Text size="sm">
|
||||
{candidate.dateOfBirth ? (
|
||||
<>
|
||||
{dayjs(candidate.dateOfBirth).format('YYYY-MM-DD')}
|
||||
{` (${dayjs().diff(dayjs(candidate.dateOfBirth), 'year')} ${t('backoffice.candidate.fields.years')})`}
|
||||
</>
|
||||
) : notAvailable}
|
||||
</Text>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Group gap="xs">
|
||||
<Text fw={500} size="sm">{t('backoffice.candidate.fields.livingingermany')}</Text>
|
||||
<Text size="sm">{yesNo(candidate.livingInGermany)}</Text>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Group gap="xs">
|
||||
<Text fw={500} size="sm">{t('backoffice.candidate.fields.jointalentpool')}</Text>
|
||||
<Text size="sm">{yesNo(candidate.joinTalentPool)}</Text>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Group gap="xs">
|
||||
<Text fw={500} size="sm">{t('backoffice.candidate.fields.worklocation')}</Text>
|
||||
<Text size="sm">{asI18n(candidate.stateWorkPreference !== null ? candidate.stateWorkPreference.join(', ') : notAvailable)}</Text>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Group gap="xs">
|
||||
<Text fw={500} size="sm">{t('backoffice.candidate.fields.submittedat')}</Text>
|
||||
<Text size="sm">{candidate.submittedAt ? asI18n(new Date(candidate.submittedAt).toLocaleDateString()) : notAvailable}</Text>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Stack gap="xl">
|
||||
{/* Education Section */}
|
||||
<Paper my="md">
|
||||
<Title order={2} mb="md">{t('enums.candidatedocumenttype.education')}</Title>
|
||||
<Stack gap="md">
|
||||
<EducationVerificationCard
|
||||
candidateId={candidateId}
|
||||
education={candidate.analysis?.education || {}}
|
||||
documents={candidate.documents?.education || []}
|
||||
verbose={verbose}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* License Section */}
|
||||
<Paper my="md">
|
||||
<Title order={2} mb="md">{t('enums.candidatedocumenttype.license')}</Title>
|
||||
<Stack gap="md">
|
||||
<LicenseVerificationCard
|
||||
candidateId={candidateId}
|
||||
license={candidate.analysis?.license || {}}
|
||||
documents={candidate.documents?.license || []}
|
||||
verbose={verbose}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Language Section */}
|
||||
<Paper my="md">
|
||||
<Title order={2} mb="md">{t('enums.candidatedocumenttype.language')}</Title>
|
||||
<Stack gap="md">
|
||||
<LanguageVerificationCard
|
||||
candidateId={candidateId}
|
||||
language={candidate.analysis?.language || {}}
|
||||
documents={candidate.documents?.language || []}
|
||||
verbose={verbose}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Experience Section */}
|
||||
<Paper my="md">
|
||||
<Title order={2} mb="md">{t('enums.candidatedocumenttype.work_experience')}</Title>
|
||||
<Stack gap="md">
|
||||
<ExperienceVerificationCard
|
||||
candidateId={candidateId}
|
||||
experience={candidate.analysis?.experience || {}}
|
||||
documents={candidate.documents?.experience || []}
|
||||
verbose={verbose}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
||||
<LifecycleNotificationsCard
|
||||
lifecycleNotifications={candidate.lifecycleNotifications}
|
||||
analysisIssuesDraft={analysisIssuesDraft}
|
||||
onAnalysisIssuesDraftChange={setAnalysisIssuesDraft}
|
||||
onSaveAnalysisIssues={saveAnalysisIssues}
|
||||
isSaving={updateCandidateMutation.isPending}
|
||||
/>
|
||||
</Stack>
|
||||
</Container>
|
||||
|
||||
{/* Results Drawer */}
|
||||
{showResultsDrawer && <Paper ml='lg' withBorder><Stack>
|
||||
<CandidateResultPreview candidateId={candidateId} />
|
||||
</Stack></Paper>}
|
||||
</Flex>
|
||||
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user