chore: heygermany customer project
This commit is contained in:
16
apps/website/views/[lang]/(app)/company/auth/login/page.tsx
Normal file
16
apps/website/views/[lang]/(app)/company/auth/login/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { LoginComponent, LoginFormData } from '@/components/LoginComponent';
|
||||
import { useCompanyLogin } from '@/hooks/company/useCompanyLogin';
|
||||
import { useRouter } from '@/framework/navigation';
|
||||
|
||||
export default function CompanyLogin() {
|
||||
const login = useCompanyLogin();
|
||||
|
||||
return (
|
||||
<LoginComponent
|
||||
namespace="company"
|
||||
mutation={login}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,772 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { useParams } from '@/framework/navigation'
|
||||
import {
|
||||
Container,
|
||||
Title,
|
||||
Text,
|
||||
Card,
|
||||
Badge,
|
||||
Group,
|
||||
Stack,
|
||||
Button,
|
||||
Box,
|
||||
Grid,
|
||||
Loader,
|
||||
Center,
|
||||
Alert,
|
||||
} from '@pikku/mantine/core'
|
||||
import {
|
||||
IconMapPin,
|
||||
IconLanguage,
|
||||
IconCalendar,
|
||||
IconBriefcase,
|
||||
IconAward,
|
||||
IconMail,
|
||||
IconInfoCircle,
|
||||
IconSchool,
|
||||
IconCheck,
|
||||
} from '@tabler/icons-react'
|
||||
import { useI18n } from '@/context/i18n-provider'
|
||||
import { asI18n } from '@pikku/react'
|
||||
import { useGetCompanyCandidate } from '@/hooks/company/useGetCompanyCandidate'
|
||||
import { useGetCountries } from '@/hooks/useGetCountries'
|
||||
import { CandidateProfileAvatar } from '@/components/company/CandidateProfileAvatar'
|
||||
|
||||
function SectionHeader({
|
||||
icon,
|
||||
title,
|
||||
meta,
|
||||
titleOrder = 3,
|
||||
}: {
|
||||
icon: ReactNode
|
||||
title: string
|
||||
meta?: ReactNode
|
||||
titleOrder?: 3 | 4
|
||||
}) {
|
||||
return (
|
||||
<Group justify="space-between" align="center" mb="lg">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
{icon}
|
||||
<Title order={titleOrder}>{asI18n(title)}</Title>
|
||||
</Group>
|
||||
{meta}
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CandidateDetailPage() {
|
||||
const params = useParams()
|
||||
const candidateId = params.candidateId as string
|
||||
const t = useI18n()
|
||||
const { getCountryNameFromAbbreviation } = useGetCountries()
|
||||
const { isLoading, data: candidate } = useGetCompanyCandidate(candidateId)
|
||||
const locale = t.locale || 'en'
|
||||
|
||||
// Format language level for display
|
||||
const formatLanguageLevel = (level: string | null) => {
|
||||
if (!level) return t('enums.languagelevel.none' as any)
|
||||
return t(`enums.languagelevel.${level.toLowerCase()}` as any)
|
||||
}
|
||||
|
||||
// Format profession for display
|
||||
const formatProfession = (profession: string | null) => {
|
||||
if (!profession) return t('common.na')
|
||||
return t(`enums.nurserecognitiongermany.${profession.toLowerCase()}` as any)
|
||||
}
|
||||
|
||||
const getRecognitionLikelihoodColor = (
|
||||
likelihood: string | null | undefined
|
||||
) => {
|
||||
if (likelihood === 'high') return 'green'
|
||||
if (likelihood === 'medium') return 'yellow'
|
||||
if (likelihood === 'low') return 'red'
|
||||
return 'gray'
|
||||
}
|
||||
|
||||
const formatExperienceDate = (date: Date | string | null | undefined) => {
|
||||
if (!date) return null
|
||||
|
||||
const parsedDate = new Date(date)
|
||||
if (Number.isNaN(parsedDate.getTime())) return null
|
||||
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
}).format(parsedDate)
|
||||
}
|
||||
|
||||
const getExperienceMonthIndex = (
|
||||
date: Date | string | null | undefined
|
||||
): number | null => {
|
||||
if (!date) return null
|
||||
|
||||
const parsedDate = new Date(date)
|
||||
if (Number.isNaN(parsedDate.getTime())) return null
|
||||
|
||||
return parsedDate.getUTCFullYear() * 12 + parsedDate.getUTCMonth()
|
||||
}
|
||||
|
||||
const getExperienceDurationMonths = (
|
||||
startDate: Date | string | null | undefined,
|
||||
endDate: Date | string | null | undefined
|
||||
): number | null => {
|
||||
const startMonth = getExperienceMonthIndex(startDate)
|
||||
const endMonth = getExperienceMonthIndex(endDate || new Date())
|
||||
|
||||
if (
|
||||
startMonth === null ||
|
||||
endMonth === null ||
|
||||
endMonth < startMonth
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return endMonth - startMonth + 1
|
||||
}
|
||||
|
||||
const formatExperienceDuration = (
|
||||
startDate: Date | string | null | undefined,
|
||||
endDate: Date | string | null | undefined
|
||||
) => {
|
||||
const totalMonths = getExperienceDurationMonths(startDate, endDate)
|
||||
if (totalMonths === null) return null
|
||||
|
||||
const years = Math.floor(totalMonths / 12)
|
||||
const months = totalMonths % 12
|
||||
const parts: string[] = []
|
||||
|
||||
if (years > 0) {
|
||||
parts.push(
|
||||
`${years} ${t(
|
||||
years === 1
|
||||
? 'company.candidates.duration.year'
|
||||
: 'company.candidates.duration.years'
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
if (months > 0) {
|
||||
parts.push(
|
||||
`${months} ${t(
|
||||
months === 1
|
||||
? 'company.candidates.duration.month'
|
||||
: 'company.candidates.duration.months'
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts.join(' ') : null
|
||||
}
|
||||
|
||||
const formatExperienceTimeline = (
|
||||
startDate: Date | string | null | undefined,
|
||||
endDate: Date | string | null | undefined
|
||||
) => {
|
||||
const start = formatExperienceDate(startDate)
|
||||
const end = endDate
|
||||
? formatExperienceDate(endDate)
|
||||
: start
|
||||
? t('company.candidate.experience.present' as any)
|
||||
: null
|
||||
|
||||
if (start && end) return `${start} - ${end}`
|
||||
return start || end || t('common.na')
|
||||
}
|
||||
|
||||
const formatExperiencePeriod = (
|
||||
startDate: Date | string | null | undefined,
|
||||
endDate: Date | string | null | undefined
|
||||
) => {
|
||||
const timeline = formatExperienceTimeline(startDate, endDate)
|
||||
const duration = formatExperienceDuration(startDate, endDate)
|
||||
|
||||
return duration ? `${timeline} · ${duration}` : timeline
|
||||
}
|
||||
|
||||
const formatFacilityType = (facilityType: string | null | undefined) => {
|
||||
if (!facilityType) return t('common.na')
|
||||
|
||||
return facilityType
|
||||
.split('_')
|
||||
.filter(Boolean)
|
||||
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
const getExperienceLocation = (
|
||||
city: string | null | undefined,
|
||||
country: string | null | undefined
|
||||
) => {
|
||||
const locationParts = [
|
||||
city,
|
||||
country
|
||||
? getCountryNameFromAbbreviation(country) || country
|
||||
: null,
|
||||
].filter(Boolean)
|
||||
|
||||
return locationParts.length > 0
|
||||
? locationParts.join(', ')
|
||||
: null
|
||||
}
|
||||
|
||||
const formatExperienceSummary = (
|
||||
city: string | null | undefined,
|
||||
country: string | null | undefined,
|
||||
facilityType: string | null | undefined
|
||||
) => {
|
||||
const parts = [
|
||||
getExperienceLocation(city, country),
|
||||
facilityType ? formatFacilityType(facilityType) : null,
|
||||
].filter(Boolean)
|
||||
|
||||
return parts.length > 0 ? parts.join(' · ') : t('common.na')
|
||||
}
|
||||
|
||||
const formatExperienceDepartments = (
|
||||
department: string | null | undefined
|
||||
) => {
|
||||
if (!department) return null
|
||||
|
||||
const departments = department
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 3)
|
||||
|
||||
return departments.length > 0 ? departments.join(', ') : null
|
||||
}
|
||||
|
||||
const formatHomeCountryQualification = (qualification: string | null) => {
|
||||
if (!qualification) return t('common.na')
|
||||
return t(
|
||||
`enums.nurserecognitionhomecountry.${qualification.toLowerCase()}` as any
|
||||
)
|
||||
}
|
||||
|
||||
const formatEducationSummary = (
|
||||
program: string | null | undefined,
|
||||
institution: string | null | undefined
|
||||
) => {
|
||||
const educationParts = [program, institution].filter(Boolean)
|
||||
return educationParts.length > 0
|
||||
? educationParts.join(' · ')
|
||||
: t('common.na')
|
||||
}
|
||||
|
||||
const formatEducationCountry = (country: string | null | undefined) =>
|
||||
country ? getCountryNameFromAbbreviation(country) || country : t('common.na')
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Center h={400}>
|
||||
<Loader size="lg" />
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
if (!candidate) {
|
||||
return (
|
||||
<Container size="xl" py="xl">
|
||||
<Alert color="red" title={t('company.candidate.notfound.title')}>
|
||||
{t('company.candidate.notfound.message')}
|
||||
</Alert>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const experiences = candidate.experience.filter((experience: any) =>
|
||||
Boolean(
|
||||
experience.roleTitle ||
|
||||
experience.employerName ||
|
||||
experience.department ||
|
||||
experience.country ||
|
||||
experience.city ||
|
||||
experience.facilityType ||
|
||||
experience.startDate ||
|
||||
experience.endDate
|
||||
)
|
||||
)
|
||||
const education = candidate.education
|
||||
const hasExperienceDetails = experiences.length > 0
|
||||
const hasEducationDetails = Boolean(
|
||||
candidate.qualificationStatusHomeCountry ||
|
||||
education?.program ||
|
||||
education?.institution ||
|
||||
education?.country ||
|
||||
typeof education?.nursingRelatedDegree === 'boolean'
|
||||
)
|
||||
const sectionCardStyle = {
|
||||
borderColor: 'var(--mantine-color-gray-2)',
|
||||
}
|
||||
const contactEmailHref = `mailto:info@hey-germany.com?subject=${encodeURIComponent(
|
||||
`${t('company.candidate.contact.emailsubject')}: ${candidate.name} (${candidate.candidateId})`
|
||||
)}&body=${encodeURIComponent(
|
||||
`${t('company.candidate.contact.emailbody')}\n\n${t('company.candidates.candidate')}: ${candidate.name}\n${t('company.candidate.contact.emailcandidateid')}: ${candidate.candidateId}\n`
|
||||
)}`
|
||||
|
||||
return (
|
||||
<Box style={{ minHeight: 'calc(100vh - 80px)' }} suppressHydrationWarning>
|
||||
<Container size="xl" py="xl" suppressHydrationWarning>
|
||||
{/* Profile Header Card */}
|
||||
<Card
|
||||
shadow="sm"
|
||||
padding="xl"
|
||||
radius="md"
|
||||
withBorder
|
||||
mb="xl"
|
||||
style={sectionCardStyle}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<Group gap="xl" wrap="nowrap" align="flex-start">
|
||||
<CandidateProfileAvatar
|
||||
countryCode={candidate.countryEducation}
|
||||
imageBorderWidth={4}
|
||||
badgeBorderWidth={4}
|
||||
badgeOffset={-8}
|
||||
badgeSize={48}
|
||||
iconSize={64}
|
||||
name={candidate.name}
|
||||
profileImageUrl={candidate.profileImageUrl}
|
||||
size={128}
|
||||
/>
|
||||
|
||||
{/* Info */}
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Title order={1} mb="xs">
|
||||
{asI18n(candidate.name)}
|
||||
</Title>
|
||||
<Text size="lg" c="gray.7" mb="md">
|
||||
{formatProfession(candidate.qualificationStatusGermany)}
|
||||
</Text>
|
||||
{candidate.recognitionLikelihood && (
|
||||
<Badge
|
||||
size="md"
|
||||
color={getRecognitionLikelihoodColor(
|
||||
candidate.recognitionLikelihood
|
||||
)}
|
||||
variant="light"
|
||||
mb="lg"
|
||||
>
|
||||
{asI18n(`${t(
|
||||
`enums.recognitionlikelihood.${candidate.recognitionLikelihood}` as any
|
||||
)} ${t('company.candidate.badge.recognitionlikelihood')}`)}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<Grid gutter="md">
|
||||
<Grid.Col span={{ base: 12, sm: 6 }}>
|
||||
<Group gap="xs">
|
||||
<IconMapPin size={18} color="var(--mantine-color-gray-6)" />
|
||||
<Text size="sm" c="gray.7">
|
||||
{asI18n(`${getCountryNameFromAbbreviation(
|
||||
candidate.countryEducation
|
||||
) || t('common.na')}${candidate.livingInGermany ? ` (${t('company.candidates.badge.ingermany')})` : ''}`)}
|
||||
</Text>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6 }}>
|
||||
<Group gap="xs">
|
||||
<IconSchool size={18} color="var(--mantine-color-gray-6)" />
|
||||
<Text size="sm" c="gray.7">
|
||||
{asI18n(education?.program || t('common.na'))}
|
||||
</Text>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6 }}>
|
||||
<Group gap="xs">
|
||||
<IconLanguage
|
||||
size={18}
|
||||
color="var(--mantine-color-gray-6)"
|
||||
/>
|
||||
<Text size="sm" c="gray.7">
|
||||
{asI18n(`${t('company.candidates.german')} ${formatLanguageLevel(
|
||||
candidate.languageSelfAssessmentLevel
|
||||
)}`)}
|
||||
</Text>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6 }}>
|
||||
<Group gap="xs">
|
||||
<IconCalendar
|
||||
size={18}
|
||||
color="var(--mantine-color-gray-6)"
|
||||
/>
|
||||
<Text size="sm" c="gray.7">
|
||||
{candidate.age
|
||||
? asI18n(`${candidate.age} ${t('company.candidate.yearsold')}`)
|
||||
: t('common.na')}
|
||||
</Text>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Group>
|
||||
</Card>
|
||||
|
||||
{/* Main Content Grid */}
|
||||
<Grid gutter="xl">
|
||||
{/* Left Column */}
|
||||
<Grid.Col span={{ base: 12, md: 8 }}>
|
||||
<Stack gap="xl">
|
||||
{/* Education Card */}
|
||||
<Card
|
||||
shadow="sm"
|
||||
padding="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
style={sectionCardStyle}
|
||||
>
|
||||
<SectionHeader
|
||||
icon={<IconSchool size={18} />}
|
||||
title={t('company.candidate.section.education')}
|
||||
/>
|
||||
|
||||
{hasEducationDetails ? (
|
||||
<Stack gap="md">
|
||||
<div>
|
||||
<Text size="sm" c="gray.6" mb={4}>
|
||||
{t('company.candidate.field.qualificationlevel')}
|
||||
</Text>
|
||||
<Text fw={500}>
|
||||
{asI18n(formatHomeCountryQualification(
|
||||
candidate.qualificationStatusHomeCountry
|
||||
))}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" c="gray.6" mb={4}>
|
||||
{t('company.candidate.field.educationdegree')}
|
||||
</Text>
|
||||
<Text fw={500}>
|
||||
{asI18n(formatEducationSummary(
|
||||
education?.program,
|
||||
education?.institution
|
||||
))}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" c="gray.6" mb={4}>
|
||||
{t('company.candidate.field.countryofeducation')}
|
||||
</Text>
|
||||
<Text fw={500}>
|
||||
{asI18n(formatEducationCountry(
|
||||
education?.country || candidate.countryEducation
|
||||
))}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" c="gray.6" mb={4}>
|
||||
{t('company.candidate.field.nursinglicense')}
|
||||
</Text>
|
||||
{candidate.hasVerifiedLicense ? (
|
||||
<Group gap="xs">
|
||||
<Text fw={500}>
|
||||
{t('company.candidate.license.verified')}
|
||||
</Text>
|
||||
<IconCheck
|
||||
size={18}
|
||||
color="var(--mantine-color-green-6)"
|
||||
/>
|
||||
</Group>
|
||||
) : (
|
||||
<Text fw={500}>
|
||||
{t('company.candidate.license.notprovided')}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
) : (
|
||||
<Text c="dimmed">{t('company.candidate.education.nodetails')}</Text>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Languages Card */}
|
||||
<Card
|
||||
shadow="sm"
|
||||
padding="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
style={sectionCardStyle}
|
||||
>
|
||||
<SectionHeader
|
||||
icon={<IconLanguage size={18} />}
|
||||
title={t('company.candidate.section.languages')}
|
||||
/>
|
||||
{candidate.languagesSpoken &&
|
||||
candidate.languagesSpoken.length > 0 && (
|
||||
<Group gap="xs" mb="md">
|
||||
{candidate.languagesSpoken.map((lang: any, index: number) => (
|
||||
<Badge key={index} variant="outline" size="md">
|
||||
{t(`enums.languagecode.${lang.toLowerCase()}` as any)}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
<Box
|
||||
pt="md"
|
||||
style={{ borderTop: '1px solid var(--mantine-color-gray-2)' }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<div>
|
||||
<Text size="sm" c="gray.6" mb={4}>
|
||||
{t('company.candidate.field.germanlevel')}
|
||||
</Text>
|
||||
<Text fw={500}>
|
||||
{asI18n(formatLanguageLevel(
|
||||
candidate.languageSelfAssessmentLevel
|
||||
))}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" c="gray.6" mb={4}>
|
||||
{t('company.candidate.field.assessmenttype')}
|
||||
</Text>
|
||||
<Text fw={500}>
|
||||
{candidate.languageCertificateProvided
|
||||
? t('company.candidate.assessmenttype.certificate')
|
||||
: t('company.candidate.assessmenttype.selfassessment')}
|
||||
</Text>
|
||||
</div>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
shadow="sm"
|
||||
padding="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
style={sectionCardStyle}
|
||||
>
|
||||
<SectionHeader
|
||||
icon={<IconBriefcase size={18} />}
|
||||
title={t('company.candidate.section.workexperience')}
|
||||
/>
|
||||
|
||||
{hasExperienceDetails ? (
|
||||
<Stack gap={0}>
|
||||
{experiences.map((experience: any, index: number) => (
|
||||
<Box
|
||||
key={`${experience.roleTitle || 'role'}-${experience.employerName || 'employer'}-${experience.startDate || index}`}
|
||||
style={{
|
||||
paddingTop: index === 0 ? 0 : 20,
|
||||
paddingBottom:
|
||||
index === experiences.length - 1 ? 0 : 20,
|
||||
borderTop:
|
||||
index === 0
|
||||
? 'none'
|
||||
: '1px solid var(--mantine-color-gray-2)',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
paddingLeft: 20,
|
||||
marginLeft: 8,
|
||||
borderLeft: '2px solid var(--mantine-color-gray-3)',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: -7,
|
||||
top: 6,
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: '50%',
|
||||
background: 'var(--mantine-color-dark-9)',
|
||||
}}
|
||||
/>
|
||||
<Stack gap={2}>
|
||||
<Text fw={600} size="lg" c="dark.9">
|
||||
{asI18n(experience.roleTitle || t('common.na'))}
|
||||
</Text>
|
||||
{experience.employerName ? (
|
||||
<Text size="md" c="dark.7">
|
||||
{asI18n(experience.employerName)}
|
||||
</Text>
|
||||
) : null}
|
||||
{experience.department ? (
|
||||
<Text size="sm" c="gray.6">
|
||||
{asI18n(formatExperienceDepartments(
|
||||
experience.department
|
||||
) || t('common.na'))}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text size="sm" c="gray.6">
|
||||
{asI18n(formatExperiencePeriod(
|
||||
experience.startDate,
|
||||
experience.endDate
|
||||
) || t('common.na'))}
|
||||
</Text>
|
||||
<Text size="sm" c="gray.6">
|
||||
{asI18n(formatExperienceSummary(
|
||||
experience.city,
|
||||
experience.country,
|
||||
experience.facilityType
|
||||
) || t('common.na'))}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Text c="dimmed">{t('company.candidate.experience.nodetails' as any)}</Text>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Recognition Status Card */}
|
||||
<Card
|
||||
shadow="sm"
|
||||
padding="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
style={sectionCardStyle}
|
||||
>
|
||||
<SectionHeader
|
||||
icon={<IconAward size={18} />}
|
||||
title={t('company.candidate.section.recognitionstatus')}
|
||||
/>
|
||||
<Stack gap="md">
|
||||
<div>
|
||||
<Group gap="xs" mb={4}>
|
||||
<Text size="sm" c="gray.6">
|
||||
{t('company.candidate.field.referenceprofession')}
|
||||
</Text>
|
||||
<IconInfoCircle
|
||||
size={16}
|
||||
color="var(--mantine-color-gray-6)"
|
||||
/>
|
||||
</Group>
|
||||
<Text fw={500}>
|
||||
{asI18n(formatProfession(candidate.qualificationStatusGermany))}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" c="gray.6" mb={4}>
|
||||
{t('company.candidate.field.recognitionlikelihood')}
|
||||
</Text>
|
||||
{candidate.recognitionLikelihood ? (
|
||||
<Badge
|
||||
size="md"
|
||||
color={getRecognitionLikelihoodColor(
|
||||
candidate.recognitionLikelihood
|
||||
)}
|
||||
variant="light"
|
||||
>
|
||||
{t(
|
||||
`enums.recognitionlikelihood.${candidate.recognitionLikelihood}` as any
|
||||
)}
|
||||
</Badge>
|
||||
) : (
|
||||
<Text fw={500}>{t('common.na')}</Text>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
|
||||
{/* Right Column */}
|
||||
<Grid.Col span={{ base: 12, md: 4 }}>
|
||||
<Stack gap="xl">
|
||||
{/* Personal Information Card */}
|
||||
<Card
|
||||
shadow="sm"
|
||||
padding="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
style={sectionCardStyle}
|
||||
>
|
||||
<SectionHeader
|
||||
icon={<IconInfoCircle size={18} />}
|
||||
title={t('company.candidate.section.personalinformation')}
|
||||
titleOrder={4}
|
||||
/>
|
||||
<Stack gap="md">
|
||||
<div>
|
||||
<Text size="sm" c="gray.6" mb={4}>
|
||||
{t('company.candidate.field.nationality')}
|
||||
</Text>
|
||||
<Text fw={500}>
|
||||
{asI18n(getCountryNameFromAbbreviation(
|
||||
candidate.nationality || candidate.countryEducation
|
||||
) || t('common.na'))}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text size="sm" c="gray.6" mb={4}>
|
||||
{t('company.candidate.field.countryofresidence')}
|
||||
</Text>
|
||||
<Text fw={500}>
|
||||
{asI18n(getCountryNameFromAbbreviation(
|
||||
candidate.currentResidence
|
||||
) || t('common.na'))}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{candidate.stateWorkPreference &&
|
||||
candidate.stateWorkPreference.length > 0 && (
|
||||
<div>
|
||||
<Text size="sm" c="gray.6" mb={4}>
|
||||
{t('company.candidate.field.preferredlocations')}
|
||||
</Text>
|
||||
<Group gap="xs" mt="xs">
|
||||
{candidate.stateWorkPreference.map((state: any, index: number) => (
|
||||
<Badge key={index} variant="outline" size="sm">
|
||||
{t(
|
||||
`enums.germanstate.${state.toLowerCase().replace(/_/g, '-')}` as any
|
||||
)}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Contact Information Card */}
|
||||
<Card
|
||||
shadow="sm"
|
||||
padding="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
style={sectionCardStyle}
|
||||
>
|
||||
<SectionHeader
|
||||
icon={<IconMail size={18} />}
|
||||
title={t('company.candidate.section.contactinformation')}
|
||||
titleOrder={4}
|
||||
/>
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="gray.7">
|
||||
{t('company.candidate.contact.emaildescription')}
|
||||
</Text>
|
||||
<Button
|
||||
component="a"
|
||||
href={contactEmailHref}
|
||||
variant="default"
|
||||
fullWidth
|
||||
leftSection={<IconMail size={18} />}
|
||||
styles={{
|
||||
root: {
|
||||
height: 48,
|
||||
borderRadius: 10,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t('company.candidate.contact.emailcta')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
587
apps/website/views/[lang]/(app)/company/candidates/page.tsx
Normal file
587
apps/website/views/[lang]/(app)/company/candidates/page.tsx
Normal file
@@ -0,0 +1,587 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import {
|
||||
Container,
|
||||
Title,
|
||||
Text,
|
||||
SimpleGrid,
|
||||
Card,
|
||||
Badge,
|
||||
Group,
|
||||
Stack,
|
||||
Checkbox,
|
||||
Drawer,
|
||||
ActionIcon,
|
||||
Loader,
|
||||
Center,
|
||||
Anchor,
|
||||
Box,
|
||||
Popover,
|
||||
} from '@pikku/mantine/core'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import {
|
||||
IconAward,
|
||||
IconBriefcase,
|
||||
IconCalendar,
|
||||
IconFilter,
|
||||
IconInfoCircle,
|
||||
IconLanguage,
|
||||
IconSchool,
|
||||
} from '@tabler/icons-react'
|
||||
import { useI18n } from '@/context/i18n-provider'
|
||||
import { asI18n } from '@pikku/react'
|
||||
import { useGetCompanyCandidates } from '@/hooks/company/useGetCompanyCandidates'
|
||||
import { useGetCountries } from '@/hooks/useGetCountries'
|
||||
import { useParams } from '@/framework/navigation'
|
||||
import { SUPPORTED_COUNTRIES } from '@heygermany/sdk/config'
|
||||
import {
|
||||
LanguageLevels,
|
||||
NurseRecognitionGermanyValues,
|
||||
RecognitionLikelihoodValues,
|
||||
type LanguageLevel,
|
||||
} from '@heygermany/sdk'
|
||||
import { CandidateProfileAvatar } from '@/components/company/CandidateProfileAvatar'
|
||||
|
||||
const LANGUAGE_LEVEL_SORT_RANK: Record<LanguageLevel, number> = {
|
||||
[LanguageLevels.NONE]: 0,
|
||||
[LanguageLevels.A_ONE]: 1,
|
||||
[LanguageLevels.A_TWO]: 2,
|
||||
[LanguageLevels.B_ONE]: 3,
|
||||
[LanguageLevels.B_TWO]: 4,
|
||||
[LanguageLevels.C_ONE]: 5,
|
||||
[LanguageLevels.C_TWO]: 6,
|
||||
}
|
||||
|
||||
const getLanguageLevelSortRank = (
|
||||
level: LanguageLevel | null | undefined
|
||||
): number => {
|
||||
if (!level) return -1
|
||||
return LANGUAGE_LEVEL_SORT_RANK[level] ?? -1
|
||||
}
|
||||
|
||||
const getCreatedAtTimestamp = (
|
||||
createdAt: string | Date | null | undefined
|
||||
): number => {
|
||||
if (!createdAt) return 0
|
||||
return new Date(createdAt).getTime()
|
||||
}
|
||||
|
||||
export default function CompanyCandidates() {
|
||||
const { isLoading, data: response } = useGetCompanyCandidates()
|
||||
const candidates = response?.data || []
|
||||
const t = useI18n()
|
||||
const { lang } = useParams()
|
||||
const { data: countriesData, getCountryNameFromAbbreviation } =
|
||||
useGetCountries()
|
||||
const [drawerOpened, { open: openDrawer, close: closeDrawer }] =
|
||||
useDisclosure(false)
|
||||
|
||||
const [selectedProfessions, setSelectedProfessions] = useState<string[]>([])
|
||||
const [selectedLanguageLevels, setSelectedLanguageLevels] = useState<
|
||||
string[]
|
||||
>([])
|
||||
const [selectedCountries, setSelectedCountries] = useState<string[]>([])
|
||||
const [selectedResidency, setSelectedResidency] = useState<string[]>([])
|
||||
|
||||
const candidateSourceCountries = SUPPORTED_COUNTRIES.filter(
|
||||
(code) => code !== 'DEU'
|
||||
)
|
||||
|
||||
const allCountries = useMemo(() => {
|
||||
if (!countriesData) return []
|
||||
|
||||
return candidateSourceCountries
|
||||
.map((code) => ({
|
||||
code,
|
||||
name: countriesData[code] || code,
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}, [countriesData])
|
||||
|
||||
const filteredCandidates = useMemo(() => {
|
||||
return candidates
|
||||
.filter((candidate: any) => {
|
||||
if (
|
||||
selectedProfessions.length > 0 &&
|
||||
!selectedProfessions.includes(
|
||||
candidate.qualificationStatusGermany || ''
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
selectedLanguageLevels.length > 0 &&
|
||||
!selectedLanguageLevels.includes(
|
||||
candidate.languageSelfAssessmentLevel || ''
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
selectedCountries.length > 0 &&
|
||||
!selectedCountries.includes(candidate.countryEducation || '')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (selectedResidency.length > 0) {
|
||||
const isInGermany = candidate.livingInGermany
|
||||
? 'in-germany'
|
||||
: 'not-in-germany'
|
||||
|
||||
if (!selectedResidency.includes(isInGermany)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
.sort((a: any, b: any) => {
|
||||
const languageLevelDifference =
|
||||
getLanguageLevelSortRank(b.languageSelfAssessmentLevel) -
|
||||
getLanguageLevelSortRank(a.languageSelfAssessmentLevel)
|
||||
|
||||
if (languageLevelDifference !== 0) {
|
||||
return languageLevelDifference
|
||||
}
|
||||
|
||||
return (
|
||||
getCreatedAtTimestamp(b.createdAt) -
|
||||
getCreatedAtTimestamp(a.createdAt)
|
||||
)
|
||||
})
|
||||
}, [
|
||||
candidates,
|
||||
selectedProfessions,
|
||||
selectedLanguageLevels,
|
||||
selectedCountries,
|
||||
selectedResidency,
|
||||
])
|
||||
|
||||
const languageLevelOptions = useMemo(
|
||||
() => t.getEnumOptions(LanguageLevels, 'languagelevel'),
|
||||
[t]
|
||||
)
|
||||
|
||||
const professionOptions = useMemo(
|
||||
() => t.getEnumOptions(NurseRecognitionGermanyValues, 'nurserecognitiongermany'),
|
||||
[t]
|
||||
)
|
||||
|
||||
const formatLanguageLevel = (level: string | null) => {
|
||||
if (!level) return t('enums.languagelevel.none' as any)
|
||||
const option = languageLevelOptions.find((opt) => opt.value === level)
|
||||
return option?.label || level
|
||||
}
|
||||
|
||||
const formatProfession = (profession: string | null) => {
|
||||
if (!profession) return t('company.candidates.profession.na' as any)
|
||||
const option = professionOptions.find((opt) => opt.value === profession)
|
||||
return option?.label || profession
|
||||
}
|
||||
|
||||
const formatEducationSummary = (program: string | null | undefined) =>
|
||||
program || t('common.na')
|
||||
|
||||
const formatTotalExperienceDuration = (
|
||||
totalExperienceMonths: number | null | undefined
|
||||
) => {
|
||||
if (!totalExperienceMonths) return t('common.na')
|
||||
|
||||
const years = Math.floor(totalExperienceMonths / 12)
|
||||
const months = totalExperienceMonths % 12
|
||||
const parts: string[] = []
|
||||
|
||||
if (years > 0) {
|
||||
parts.push(
|
||||
`${years} ${t(
|
||||
years === 1
|
||||
? 'company.candidates.duration.year'
|
||||
: 'company.candidates.duration.years'
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
if (months > 0 || parts.length === 0) {
|
||||
parts.push(
|
||||
`${months} ${t(
|
||||
months === 1
|
||||
? 'company.candidates.duration.month'
|
||||
: 'company.candidates.duration.months'
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
return parts.join(' ')
|
||||
}
|
||||
|
||||
const getRecognitionLikelihoodBadge = (likelihood: string | null) => {
|
||||
if (!likelihood) return null
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
[RecognitionLikelihoodValues.HIGH]: 'green',
|
||||
[RecognitionLikelihoodValues.MEDIUM]: 'yellow',
|
||||
[RecognitionLikelihoodValues.LOW]: 'red',
|
||||
}
|
||||
|
||||
return {
|
||||
color: colorMap[likelihood] || 'gray',
|
||||
label: t(`enums.recognitionlikelihood.${likelihood}` as any),
|
||||
}
|
||||
}
|
||||
|
||||
const FilterContent = () => (
|
||||
<Stack gap="xl">
|
||||
<div>
|
||||
<Group gap={6} mb="sm">
|
||||
<Text fw={500} size="sm" c="dark.7">
|
||||
{t('company.candidates.filters.profession')}
|
||||
</Text>
|
||||
<Popover width={320} position="right" withArrow shadow="md">
|
||||
<Popover.Target>
|
||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||
<IconInfoCircle size={14} />
|
||||
</ActionIcon>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Text size="sm" style={{ whiteSpace: 'pre-line', lineHeight: 1.6 }}>
|
||||
{t('company.candidates.filters.profession.tooltip' as any)}
|
||||
</Text>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</Group>
|
||||
<Checkbox.Group
|
||||
value={selectedProfessions}
|
||||
onChange={setSelectedProfessions}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{professionOptions.map((option) => (
|
||||
<Checkbox
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
label={asI18n(option.label)}
|
||||
size="sm"
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Checkbox.Group>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text fw={500} size="sm" mb="sm" c="dark.7">
|
||||
{t('company.candidates.filters.language')}
|
||||
</Text>
|
||||
<Checkbox.Group
|
||||
value={selectedLanguageLevels}
|
||||
onChange={setSelectedLanguageLevels}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{languageLevelOptions.map((option) => (
|
||||
<Checkbox
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
label={asI18n(option.label)}
|
||||
size="sm"
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Checkbox.Group>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text fw={500} size="sm" mb="sm" c="dark.7">
|
||||
{t('company.candidates.filters.country')}
|
||||
</Text>
|
||||
<Checkbox.Group
|
||||
value={selectedCountries}
|
||||
onChange={setSelectedCountries}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{allCountries.map(({ code, name }) => (
|
||||
<Checkbox key={code} value={code} label={asI18n(name)} size="sm" />
|
||||
))}
|
||||
</Stack>
|
||||
</Checkbox.Group>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text fw={500} size="sm" mb="sm" c="dark.7">
|
||||
{t('company.candidates.filters.residency')}
|
||||
</Text>
|
||||
<Checkbox.Group
|
||||
value={selectedResidency}
|
||||
onChange={setSelectedResidency}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Checkbox
|
||||
value="in-germany"
|
||||
label={t('company.candidates.filters.iningermany')}
|
||||
size="sm"
|
||||
/>
|
||||
<Checkbox
|
||||
value="not-in-germany"
|
||||
label={t('company.candidates.filters.notingermany')}
|
||||
size="sm"
|
||||
/>
|
||||
</Stack>
|
||||
</Checkbox.Group>
|
||||
</div>
|
||||
</Stack>
|
||||
)
|
||||
|
||||
const filterPanelScrollStyle = {
|
||||
minHeight: 0,
|
||||
overflowY: 'auto' as const,
|
||||
paddingRight: 4,
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Center h={400}>
|
||||
<Loader size="lg" />
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
opened={drawerOpened}
|
||||
onClose={closeDrawer}
|
||||
title={
|
||||
<Text fw={600} size="lg">
|
||||
{t('company.candidates.filters.title')}
|
||||
</Text>
|
||||
}
|
||||
padding="lg"
|
||||
size="sm"
|
||||
hiddenFrom="lg"
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
...filterPanelScrollStyle,
|
||||
maxHeight: 'calc(100dvh - 140px)',
|
||||
}}
|
||||
>
|
||||
<FilterContent />
|
||||
</Box>
|
||||
</Drawer>
|
||||
|
||||
<Box style={{ minHeight: 'calc(100vh - 80px)' }}>
|
||||
<Container size="xl" py="xl">
|
||||
<Group align="flex-start" gap="xl">
|
||||
<Card
|
||||
shadow="sm"
|
||||
padding="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
w={280}
|
||||
visibleFrom="lg"
|
||||
style={{
|
||||
position: 'sticky',
|
||||
top: 100,
|
||||
maxHeight: 'calc(100dvh - 120px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Text fw={600} size="lg" mb="lg">
|
||||
{t('company.candidates.filters.title')}
|
||||
</Text>
|
||||
<Box style={filterPanelScrollStyle}>
|
||||
<FilterContent />
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Group justify="space-between" mb="xl">
|
||||
<Title order={2}>
|
||||
{t('company.candidates.candidatesfound' as any, {
|
||||
count: filteredCandidates.length,
|
||||
})}
|
||||
</Title>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
onClick={openDrawer}
|
||||
hiddenFrom="lg"
|
||||
>
|
||||
<IconFilter size={20} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
|
||||
{filteredCandidates.map((candidate: any) => {
|
||||
const recognitionBadge = getRecognitionLikelihoodBadge(
|
||||
candidate.recognitionLikelihood
|
||||
)
|
||||
|
||||
return (
|
||||
<Anchor
|
||||
key={candidate.candidateId}
|
||||
href={`/${lang}/company/candidate/${candidate.candidateId}`}
|
||||
underline="never"
|
||||
style={{ display: 'block', height: '100%' }}
|
||||
>
|
||||
<Card
|
||||
shadow="sm"
|
||||
padding="xl"
|
||||
radius="md"
|
||||
withBorder
|
||||
h="100%"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
position: 'relative',
|
||||
}}
|
||||
styles={{
|
||||
root: {
|
||||
'&:hover': {
|
||||
boxShadow: '0 12px 24px rgba(0, 0, 0, 0.12)',
|
||||
transform: 'translateY(-4px)',
|
||||
borderColor: 'var(--mantine-color-blue-5)',
|
||||
backgroundColor: 'var(--mantine-color-gray-0)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{candidate.livingInGermany && (
|
||||
<Box
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
right: 16,
|
||||
backgroundColor: 'var(--mantine-color-gray-1)',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
<Text size="xs" c="black" tt="uppercase" fw={500}>
|
||||
{t('company.candidates.card.in_germany')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Group gap="md" mb="md" wrap="nowrap" align="flex-start">
|
||||
<CandidateProfileAvatar
|
||||
countryCode={candidate.countryEducation}
|
||||
imageBorderWidth={2}
|
||||
badgeBorderWidth={2}
|
||||
badgeOffset={-2}
|
||||
badgeSize={28}
|
||||
iconSize={28}
|
||||
name={candidate.name}
|
||||
profileImageUrl={candidate.profileImageUrl}
|
||||
size={56}
|
||||
/>
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text fw={600} size="lg" mb={4} c="dark.9">
|
||||
{candidate.name}
|
||||
</Text>
|
||||
<Text size="md" c="gray.6">
|
||||
{asI18n(getCountryNameFromAbbreviation(
|
||||
candidate.countryEducation
|
||||
) || t('common.na'))}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
<Stack gap="xs">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<IconBriefcase
|
||||
size={18}
|
||||
color="var(--mantine-color-gray-6)"
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
<Text size="md" c="gray.7" lineClamp={1}>
|
||||
{asI18n(formatProfession(
|
||||
candidate.qualificationStatusGermany
|
||||
))}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<Group gap="xs" wrap="nowrap" align="flex-start">
|
||||
<IconSchool
|
||||
size={18}
|
||||
color="var(--mantine-color-gray-6)"
|
||||
style={{ flexShrink: 0, marginTop: 2 }}
|
||||
/>
|
||||
<Text size="md" c="gray.7" lineClamp={2}>
|
||||
{asI18n(formatEducationSummary(
|
||||
candidate.education?.program
|
||||
))}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<IconLanguage
|
||||
size={18}
|
||||
color="var(--mantine-color-gray-6)"
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
<Text size="md" c="gray.7">
|
||||
{asI18n(`${t('company.candidates.card.germanlevel' as any)}: ${formatLanguageLevel(candidate.languageSelfAssessmentLevel)}`)}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<IconCalendar
|
||||
size={18}
|
||||
color="var(--mantine-color-gray-6)"
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
<Text size="md" c="gray.7">
|
||||
{asI18n(`${t('company.candidates.card.totalexperience')}: ${formatTotalExperienceDuration(candidate.totalExperienceMonths)}`)}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
{recognitionBadge ? (
|
||||
<Box
|
||||
mt="sm"
|
||||
pt="sm"
|
||||
style={{
|
||||
borderTop: '1px solid var(--mantine-color-gray-2)',
|
||||
}}
|
||||
>
|
||||
<Group gap="xs" align="center">
|
||||
<IconAward
|
||||
size={18}
|
||||
color="var(--mantine-color-gray-6)"
|
||||
/>
|
||||
<Text size="md" c="gray.7" fw={500}>
|
||||
{asI18n(`${t('company.candidates.card.recognitionlikelihood' as any)}:`)}
|
||||
</Text>
|
||||
<Badge
|
||||
size="sm"
|
||||
color={recognitionBadge.color}
|
||||
variant="light"
|
||||
>
|
||||
{asI18n(recognitionBadge.label)}
|
||||
</Badge>
|
||||
</Group>
|
||||
</Box>
|
||||
) : null}
|
||||
</Card>
|
||||
</Anchor>
|
||||
)
|
||||
})}
|
||||
</SimpleGrid>
|
||||
|
||||
{filteredCandidates.length === 0 && (
|
||||
<Center h={200}>
|
||||
<Text c="dimmed" size="lg">
|
||||
{t('company.candidates.nocandidates')}
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
</Group>
|
||||
</Container>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
67
apps/website/views/[lang]/(app)/company/layout.tsx
Normal file
67
apps/website/views/[lang]/(app)/company/layout.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
|
||||
import { AppShell, Flex, Text, Container, Button, Box } from '@pikku/mantine/core';
|
||||
import { usePathname, useRouter, useParams } from '@/framework/navigation';
|
||||
import { IconArrowLeft } from '@tabler/icons-react';
|
||||
import LanguageSelector from '@/components/ui/LanguageSelector';
|
||||
import Logo from '@/components/ui/Logo';
|
||||
import { useI18n } from '@/context/i18n-provider';
|
||||
|
||||
interface CompanyLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function CompanyLayout({ children }: CompanyLayoutProps) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const t = useI18n();
|
||||
const lang = params.lang as string;
|
||||
const isDetailPage = pathname.includes('/company/candidate/');
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
padding={0}
|
||||
header={{ height: 80 }}
|
||||
styles={{
|
||||
main: {
|
||||
backgroundColor: 'var(--mantine-color-gray-0)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AppShell.Header>
|
||||
{isDetailPage ? (
|
||||
<Box style={{ borderBottom: '1px solid var(--mantine-color-gray-2)', height: '100%' }}>
|
||||
<Container size="xl" h="100%">
|
||||
<Flex align="center" justify="space-between" h="100%">
|
||||
<Button
|
||||
variant="default"
|
||||
leftSection={<IconArrowLeft size={16} />}
|
||||
onClick={() => router.push(`/${lang}/company/candidates`)}
|
||||
>
|
||||
{t('company.navigation.backtosearch')}
|
||||
</Button>
|
||||
<LanguageSelector />
|
||||
</Flex>
|
||||
</Container>
|
||||
</Box>
|
||||
) : (
|
||||
<Flex align="center" justify="space-between" h="100%" px="xl">
|
||||
<div>
|
||||
<Logo size="sm" />
|
||||
<Text size="sm" c="dimmed" mt={2}>
|
||||
{t('company.navigation.tagline')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Flex gap="sm" align="center">
|
||||
<LanguageSelector />
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</AppShell.Header>
|
||||
|
||||
<AppShell.Main>{children}</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
15
apps/website/views/[lang]/(app)/company/page.tsx
Normal file
15
apps/website/views/[lang]/(app)/company/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter, useParams } from '@/framework/navigation';
|
||||
|
||||
export default function CompanyPage() {
|
||||
const router = useRouter();
|
||||
const { lang } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
router.replace(`/${lang}/company/candidates`);
|
||||
}, [router, lang]);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user