404 lines
12 KiB
TypeScript
404 lines
12 KiB
TypeScript
import { TextInput, Select, Textarea, NumberInput, Group, Text, MultiSelect } from '@pikku/mantine/core';
|
|
import { DateInput } from '@mantine/dates';
|
|
import {
|
|
ApplicantStatuses,
|
|
CandidateDocumentTypes,
|
|
LanguageLevels,
|
|
NurseRecognitionGermanyValues,
|
|
NurseRecognitionHomeCountryValues,
|
|
} from '@heygermany/sdk';
|
|
import React from 'react';
|
|
import { EnumSelect } from '@/components/common/EnumSelect';
|
|
import { useI18n } from '@/context/i18n-provider';
|
|
import { asI18n } from '@pikku/react';
|
|
|
|
export type FieldComponentProps = {
|
|
value: any;
|
|
onChange: (value: any) => void;
|
|
label?: any;
|
|
placeholder?: any;
|
|
disabled?: boolean;
|
|
};
|
|
|
|
export type FieldComponent = (props: FieldComponentProps) => React.ReactElement;
|
|
|
|
// Reusable component functions
|
|
export const TextInputComponent: FieldComponent = ({ value, onChange, label, placeholder, disabled }) => (
|
|
<TextInput
|
|
value={value || ''}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
label={label as any}
|
|
placeholder={placeholder as any}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
|
|
export const TextareaComponent: FieldComponent = ({ value, onChange, label, placeholder, disabled }) => (
|
|
<Textarea
|
|
value={value || ''}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
label={label as any}
|
|
placeholder={placeholder as any}
|
|
disabled={disabled}
|
|
autosize
|
|
minRows={2}
|
|
/>
|
|
);
|
|
|
|
export const NumberInputComponent: FieldComponent = ({ value, onChange, label, placeholder, disabled }) => (
|
|
<NumberInput
|
|
value={value || undefined}
|
|
onChange={onChange}
|
|
label={label as any}
|
|
placeholder={placeholder as any}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
|
|
export const DateInputComponent: FieldComponent = ({ value, onChange, label, placeholder, disabled }) => (
|
|
<DateInput
|
|
value={value ? new Date(value) : null}
|
|
onChange={(date: string | null) => onChange(date)}
|
|
label={label as any}
|
|
placeholder={placeholder as any}
|
|
disabled={disabled}
|
|
valueFormat="YYYY-MM-DD"
|
|
/>
|
|
);
|
|
|
|
export const LanguageLevelSelectComponent: FieldComponent = ({ value, onChange, label, disabled }) => {
|
|
const t = useI18n();
|
|
return (
|
|
<EnumSelect
|
|
enumObject={LanguageLevels}
|
|
enumName="LanguageLevel"
|
|
value={value || null}
|
|
onChange={onChange}
|
|
label={label}
|
|
placeholder={t('backoffice.filters.selectlevel')}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const BooleanSelectComponent: FieldComponent = ({ value, onChange, label, disabled }) => {
|
|
const t = useI18n();
|
|
const options = [
|
|
{ value: 'true', label: t('common.yes') },
|
|
{ value: 'false', label: t('common.no') },
|
|
{ value: 'null', label: t('backoffice.filters.unknown') }
|
|
];
|
|
|
|
const getCurrentValue = () => {
|
|
if (value === true) return 'true';
|
|
if (value === false) return 'false';
|
|
return 'null';
|
|
};
|
|
|
|
const handleChange = (newValue: string | null) => {
|
|
if (newValue === 'true') onChange(true);
|
|
else if (newValue === 'false') onChange(false);
|
|
else onChange(null);
|
|
};
|
|
|
|
return (
|
|
<Select
|
|
value={getCurrentValue()}
|
|
onChange={handleChange}
|
|
label={label as any}
|
|
placeholder={t('backoffice.filters.select')}
|
|
disabled={disabled}
|
|
data={options}
|
|
styles={{
|
|
input: {
|
|
...(value === null && !disabled ? {
|
|
borderColor: '#FFA500',
|
|
borderWidth: '2px'
|
|
} : {})
|
|
}
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const StringArrayComponent: FieldComponent = ({ value, onChange, label, disabled }) => {
|
|
const t = useI18n();
|
|
const arrayValue = Array.isArray(value) ? value : [];
|
|
|
|
return (
|
|
<Group gap="xs" align="flex-start">
|
|
<Text size="sm" fw={500}>{asI18n(String(label ?? ''))}</Text>
|
|
<Textarea
|
|
value={arrayValue.join('\n')}
|
|
onChange={(e) => onChange(e.target.value.split('\n').filter(item => item.trim()))}
|
|
placeholder={t('backoffice.filters.oneitemperline')}
|
|
disabled={disabled}
|
|
autosize
|
|
minRows={2}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
</Group>
|
|
);
|
|
};
|
|
|
|
export const NurseRecognitionGermanySelectComponent: FieldComponent = ({ value, onChange, label, disabled }) => {
|
|
const t = useI18n();
|
|
return (
|
|
<EnumSelect
|
|
enumObject={NurseRecognitionGermanyValues}
|
|
enumName="NurseRecognitionGermany"
|
|
value={value || null}
|
|
onChange={onChange}
|
|
label={label}
|
|
placeholder={t('backoffice.filters.selectrecognitionlevel')}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const NurseRecognitionHomeCountrySelectComponent: FieldComponent = ({ value, onChange, label, disabled }) => {
|
|
const t = useI18n();
|
|
return (
|
|
<EnumSelect
|
|
enumObject={NurseRecognitionHomeCountryValues}
|
|
enumName="NurseRecognitionHomeCountry"
|
|
value={value || null}
|
|
onChange={onChange}
|
|
label={label}
|
|
placeholder={t('backoffice.filters.selectrecognitionlevel')}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const ApplicantStatusSelectComponent: FieldComponent = ({ value, onChange, label, placeholder, disabled }) => {
|
|
const t = useI18n();
|
|
return (
|
|
<EnumSelect
|
|
enumObject={ApplicantStatuses}
|
|
enumName="ApplicantStatus"
|
|
value={value || null}
|
|
onChange={onChange}
|
|
label={label}
|
|
placeholder={placeholder || t('backoffice.candidate.fields.selectstatus')}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const WhatItMeansSelectComponent: FieldComponent = ({ value, onChange, label, disabled }) => {
|
|
const t = useI18n();
|
|
// Define the enum values based on the SQL schema
|
|
const WhatItMeansEnum = {
|
|
PARTIAL_RECOGNITION_B_ONE: 'PARTIAL_RECOGNITION_B_ONE',
|
|
PARTIAL_RECOGNITION_B_TWO: 'PARTIAL_RECOGNITION_B_TWO',
|
|
NONE: 'NONE',
|
|
} as const;
|
|
|
|
const whatItMeansOptions = Object.values(WhatItMeansEnum).map(meaning => ({
|
|
value: meaning,
|
|
label: meaning.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase())
|
|
}));
|
|
|
|
return (
|
|
<Select
|
|
value={value || null}
|
|
onChange={onChange}
|
|
label={label as any}
|
|
placeholder={t('backoffice.filters.selectmeaning')}
|
|
disabled={disabled}
|
|
data={whatItMeansOptions}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const NextStepsArraySelectComponent: FieldComponent = ({ value, onChange, label, disabled }) => {
|
|
const t = useI18n();
|
|
const arrayValue = Array.isArray(value) ? value : [];
|
|
|
|
// Define the enum values based on the SQL schema
|
|
const NextStepsEnum = {
|
|
IMPROVE_GERMAN_B_TWO: 'IMPROVE_GERMAN_B_TWO',
|
|
IMPROVE_GERMAN_B_ONE: 'IMPROVE_GERMAN_B_ONE',
|
|
SUBMIT_APPLICATION_ASSISTANT: 'SUBMIT_APPLICATION_ASSISTANT',
|
|
GET_HELP: 'GET_HELP',
|
|
CONNECT_WITH_EMPLOYERS: 'CONNECT_WITH_EMPLOYERS',
|
|
} as const;
|
|
|
|
const nextStepsOptions = Object.values(NextStepsEnum).map(step => ({
|
|
value: step,
|
|
label: step.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase())
|
|
}));
|
|
|
|
return (
|
|
<MultiSelect
|
|
label={label as any}
|
|
placeholder={t('backoffice.filters.selectsteps')}
|
|
value={arrayValue}
|
|
onChange={(val) => onChange(val || [])}
|
|
data={nextStepsOptions}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const MinimumGermanLevelSelectComponent: FieldComponent = ({ value, onChange, label, disabled }) => {
|
|
const t = useI18n();
|
|
return (
|
|
<EnumSelect
|
|
enumObject={LanguageLevels}
|
|
enumName="LanguageLevel"
|
|
value={value || null}
|
|
onChange={onChange}
|
|
label={label}
|
|
placeholder={t('backoffice.filters.selectlevel')}
|
|
disabled={disabled}
|
|
/>
|
|
);
|
|
};
|
|
|
|
// Duration input component that converts text like "1y 5m" to hours
|
|
export const DurationInputComponent: FieldComponent = ({ value, onChange, label, placeholder, disabled }) => {
|
|
const t = useI18n();
|
|
const [inputValue, setInputValue] = React.useState('');
|
|
|
|
// Convert hours to display format
|
|
const hoursToDisplayFormat = (hours: number): string => {
|
|
if (!hours || hours === 0) return '';
|
|
|
|
let remaining = hours;
|
|
const parts = [];
|
|
|
|
// Calculate years
|
|
const years = Math.floor(remaining / (365 * 24));
|
|
if (years > 0) {
|
|
parts.push(`${years}y`);
|
|
remaining -= years * 365 * 24;
|
|
}
|
|
|
|
// Calculate months
|
|
const months = Math.floor(remaining / (30 * 24));
|
|
if (months > 0) {
|
|
parts.push(`${months}m`);
|
|
remaining -= months * 30 * 24;
|
|
}
|
|
|
|
// Calculate days
|
|
const days = Math.floor(remaining / 24);
|
|
if (days > 0) {
|
|
parts.push(`${days}d`);
|
|
remaining -= days * 24;
|
|
}
|
|
|
|
// Remaining hours
|
|
if (remaining > 0) {
|
|
parts.push(`${remaining}h`);
|
|
}
|
|
|
|
return parts.join(' ') || '0h';
|
|
};
|
|
|
|
// Convert display format to hours
|
|
const displayFormatToHours = (input: string): number => {
|
|
if (!input) return 0;
|
|
|
|
let totalHours = 0;
|
|
const yearMatch = input.match(/(\d+)y/);
|
|
const monthMatch = input.match(/(\d+)m/);
|
|
const dayMatch = input.match(/(\d+)d/);
|
|
const hourMatch = input.match(/(\d+)h/);
|
|
|
|
if (yearMatch) totalHours += parseInt(yearMatch[1]) * 365 * 24;
|
|
if (monthMatch) totalHours += parseInt(monthMatch[1]) * 30 * 24;
|
|
if (dayMatch) totalHours += parseInt(dayMatch[1]) * 24;
|
|
if (hourMatch) totalHours += parseInt(hourMatch[1]);
|
|
|
|
return totalHours;
|
|
};
|
|
|
|
// Initialize input value from props
|
|
React.useEffect(() => {
|
|
if (value !== undefined && value !== null) {
|
|
setInputValue(hoursToDisplayFormat(value));
|
|
}
|
|
}, [value]);
|
|
|
|
const handleInputChange = (newInputValue: string) => {
|
|
setInputValue(newInputValue);
|
|
const hours = displayFormatToHours(newInputValue);
|
|
onChange(hours);
|
|
};
|
|
|
|
return (
|
|
<Group gap="xs" align="flex-end">
|
|
<TextInput
|
|
value={inputValue}
|
|
onChange={(e) => handleInputChange(e.target.value)}
|
|
label={label as any}
|
|
placeholder={placeholder || t('backoffice.filters.durationexample')}
|
|
disabled={disabled}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<Text size="xs" c="dimmed" pb="xs">
|
|
{value ? asI18n(`${value} hours`) : asI18n('')}
|
|
</Text>
|
|
</Group>
|
|
);
|
|
};
|
|
|
|
// Field metadata mapping using enum values directly
|
|
export const FIELD_METADATA: Record<string, Record<string, FieldComponent>> = {
|
|
// Nursing License fields
|
|
[CandidateDocumentTypes.LICENSE]: {
|
|
licenseNumber: TextInputComponent,
|
|
licenseType: TextInputComponent,
|
|
issuingAuthority: TextInputComponent,
|
|
issueDate: DateInputComponent,
|
|
expiryDate: DateInputComponent,
|
|
status: TextInputComponent,
|
|
},
|
|
|
|
// Training Certificate fields
|
|
[CandidateDocumentTypes.EDUCATION]: {
|
|
institution: TextInputComponent,
|
|
program: TextInputComponent,
|
|
country: TextInputComponent,
|
|
completionDate: DateInputComponent,
|
|
nursingRelatedDegree: BooleanSelectComponent,
|
|
accreditedUniversity: BooleanSelectComponent,
|
|
accreditedStudyProgram: BooleanSelectComponent,
|
|
hasTheory: BooleanSelectComponent,
|
|
hasPractice: BooleanSelectComponent,
|
|
durationHours: DurationInputComponent,
|
|
isNursingFocus: BooleanSelectComponent,
|
|
},
|
|
|
|
// German Language Certificate fields
|
|
[CandidateDocumentTypes.LANGUAGE]: {
|
|
level: LanguageLevelSelectComponent,
|
|
issuingBody: TextInputComponent,
|
|
issueDate: DateInputComponent,
|
|
certifiedSkills: StringArrayComponent,
|
|
},
|
|
|
|
// Work Experience fields
|
|
[CandidateDocumentTypes.WORK_EXPERIENCE]: {
|
|
employer: TextInputComponent,
|
|
position: TextInputComponent,
|
|
department: TextInputComponent,
|
|
startDate: DateInputComponent,
|
|
endDate: DateInputComponent,
|
|
durationMonths: NumberInputComponent,
|
|
isFullTime: BooleanSelectComponent,
|
|
duties: StringArrayComponent,
|
|
},
|
|
|
|
// Nursing Qualification Rule fields
|
|
'nursing_qualification_rule': {
|
|
},
|
|
};
|
|
|
|
export function getFieldComponent(documentType: string, fieldName: string): FieldComponent {
|
|
return FIELD_METADATA[documentType]?.[fieldName] || TextInputComponent;
|
|
}
|