Files
e2e 36f1b746bd
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 11:03:59 +02:00

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;
}