chore: heygermany customer project
This commit is contained in:
229
apps/website/components/jobs/results/ApplicantProfile.tsx
Normal file
229
apps/website/components/jobs/results/ApplicantProfile.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
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
|
||||
Reference in New Issue
Block a user