Files
e2e aae77ea31e
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 09:28:50 +02:00

376 lines
16 KiB
TypeScript

'use client';
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 '@/framework/navigation';
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();
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>
</>
);
}