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