Files
e2e 3bb535efe8
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:35:04 +02:00

230 lines
9.4 KiB
TypeScript

import React, { useMemo, useState } from 'react'
import { Controller, useForm } from 'react-hook-form'
import { Box, Button, Checkbox, SimpleGrid, Stack, Text } from '@pikku/mantine/core'
import type { DB } from '@heygermany/sdk'
import {
ApplicationCandidateData,
CandidateDocumentTypes,
LanguageCodes,
ProfessionalRecognitionStatuses,
} from '@heygermany/sdk'
import { useI18n } from '@/context/i18n-provider'
import { pikku } from '@/pikku/http'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { EnumSelect } from '@/components/common/EnumSelect'
import { FileUpload } from '@/components/ui/FileUpload'
import { useUploadCandidateFiles } from '@/hooks/useUploadCandidateFiles'
import { useLocation, useParams } from '@tanstack/react-router'
import { CountrySearch } from '../form/CountrySearch'
interface ApplicantProfileForm {
nationality: string
currentResidence: string
professionalRecognitionStatus: DB.ProfessionalRecognitionStatus | null
qualificationStatusGermany: DB.NurseRecognitionGermany | null
recognitionLikelihood: DB.RecognitionLikelihood | null
languagesSpoken: string[]
}
export const ApplicantProfile: React.FunctionComponent = () => {
const params = useParams({ strict: false })
const searchParams = useLocation({ select: (l) => new URLSearchParams(l.searchStr) })
const candidateIdFromUrl = params.candidateId as string | undefined
const candidateIdFromQuery = searchParams.get('candidateId')
const candidateId = candidateIdFromUrl || candidateIdFromQuery || undefined
// Fetch existing candidate data
const { data: candidate } = useQuery({
queryKey: ['candidate', candidateId],
queryFn: async () => {
if (candidateId) {
return await pikku().get('/candidate/:candidateId', { candidateId })
}
return await pikku().get('/candidate')
},
})
if (!candidate) {
return null
}
return <ApplicantProfileInner candidate={candidate} />
}
const ApplicantProfileInner: React.FunctionComponent<{ candidate: ApplicationCandidateData }> = ({ candidate }) => {
const t = useI18n()
const queryClient = useQueryClient()
const [isSaving, setIsSaving] = useState(false)
// Update mutation
const updateMutation = useMutation({
mutationFn: async (data: ApplicantProfileForm) => {
if (!candidate?.candidateId) {
throw new Error('Candidate ID not found')
}
setIsSaving(true)
// Create a minimum delay promise (2 seconds)
const minDelay = new Promise(resolve => setTimeout(resolve, 2000))
// Update candidate profile
const updatePromise = pikku().patch(`/candidate/:candidateId`, {
candidateId: candidate.candidateId,
...data,
languagesSpoken: data.languagesSpoken as DB.LanguageCode[],
})
// Wait for both the update and minimum delay
await Promise.all([updatePromise, minDelay])
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['candidate'] })
setIsSaving(false)
// Show success notification or redirect
},
onError: () => {
setIsSaving(false)
},
})
const { control, handleSubmit, trigger, formState: { errors } } = useForm<ApplicantProfileForm>({
defaultValues: {
nationality: candidate.nationality || '',
currentResidence: candidate.currentResidence || '',
professionalRecognitionStatus: candidate.professionalRecognitionStatus || null,
qualificationStatusGermany: candidate.qualificationStatusGermany || null,
recognitionLikelihood: candidate.recognitionLikelihood || null,
languagesSpoken: candidate.languagesSpoken || [],
},
})
// File upload for CV (using WORK_EXPERIENCE document type)
const { uploadFiles, deleteFile } = useUploadCandidateFiles(
candidate.candidateId,
CandidateDocumentTypes.WORK_EXPERIENCE,
trigger
)
// Available languages (sorted alphabetically, English first)
const selectEnums = t.getEnumOptions(LanguageCodes, 'languagecode')
const availableLanguages = useMemo(() => {
// English first, then sort rest alphabetically by label
const english = selectEnums.find(l => l.value === 'en')!
const others = selectEnums.filter(l => l.value !== 'en').sort((a, b) => a.label.localeCompare(b.label))
return [english, ...others]
}, [t, selectEnums])
return (
<>
<Text size="sm" c="dimmed" mb="lg">
{t('jobs.profile.description')}
</Text>
<form onSubmit={handleSubmit((data) => updateMutation.mutate(data))}>
<Stack gap="md">
{/* Nationality */}
<Controller
name="nationality"
control={control}
render={({ field }) => (
<CountrySearch
label={t('jobs.profile.nationality.label')}
placeholder={t('jobs.profile.nationality.placeholder')}
value={field.value}
onChange={field.onChange}
error={errors.nationality?.message}
/>
)}
/>
{/* Current Residence */}
<Controller
name="currentResidence"
control={control}
render={({ field }) => (
<CountrySearch
label={t('jobs.profile.residence.label')}
placeholder={t('jobs.profile.residence.placeholder')}
value={field.value}
onChange={field.onChange}
error={errors.currentResidence?.message}
/>
)}
/>
{/* Professional Recognition Status */}
<Controller
name="professionalRecognitionStatus"
control={control}
render={({ field }) => (
<EnumSelect
label={t('jobs.profile.recognitionstatus.label')}
enumObject={ProfessionalRecognitionStatuses}
enumName='professionalRecognitionStatus'
value={field.value}
onChange={field.onChange}
error={errors.professionalRecognitionStatus?.message} />
)}
/>
{/* Languages Spoken */}
<Box>
<Text size="sm" fw={500} mb="xs">{t('jobs.profile.languages.label')}</Text>
<Controller
name="languagesSpoken"
control={control}
render={({ field }) => (
<Checkbox.Group
value={field.value}
onChange={field.onChange}
error={errors.languagesSpoken?.message}
>
<SimpleGrid cols={3} spacing="sm">
{availableLanguages.map((lang) => (
<Checkbox
key={lang.value}
value={lang.value}
label={lang.label}
/>
))}
</SimpleGrid>
</Checkbox.Group>
)}
/>
</Box>
{/* CV Upload */}
<Box>
<Text size="sm" fw={500} mb="xs">{t('jobs.profile.cv.label')}</Text>
<Text size="sm" c="dimmed" mb="sm">
{t('jobs.profile.cv.description')}
</Text>
<FileUpload
files={candidate?.workExperience?.map((file: DB.CandidateDocument) => ({
id: file.documentId,
fileName: file.fileName
})) || []}
uploadFiles={uploadFiles}
deleteFile={deleteFile}
/>
</Box>
{/* Submit Button */}
<Button
type="submit"
size="lg"
w='100%'
mt="md"
loading={isSaving}
>
{t('jobs.profile.savebutton')}
</Button>
</Stack>
</form>
</>
)
}
export default ApplicantProfile