230 lines
9.4 KiB
TypeScript
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
|