Files
zentral/frontend/src/components/customFields/CustomFieldsRenderer.tsx
2025-11-30 00:34:56 +01:00

198 lines
5.4 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import {
TextField,
Checkbox,
FormControlLabel,
MenuItem,
Typography,
Box
} from '@mui/material';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { CustomFieldDefinition, CustomFieldType, CustomFieldValues } from '../../types/customFields';
import { useTranslation } from 'react-i18next';
import { customFieldService } from '../../services/customFieldService';
import dayjs from 'dayjs';
interface Props {
entityName: string;
values: CustomFieldValues;
onChange: (fieldName: string, value: any) => void;
readOnly?: boolean;
}
export const CustomFieldsRenderer: React.FC<Props> = ({ entityName, values, onChange, readOnly = false }) => {
const { t } = useTranslation();
const [definitions, setDefinitions] = useState<CustomFieldDefinition[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadDefinitions = async () => {
try {
const defs = await customFieldService.getByEntity(entityName);
setDefinitions(defs);
} catch (error) {
console.error("Failed to load custom fields", error);
} finally {
setLoading(false);
}
};
loadDefinitions();
}, [entityName]);
if (loading) return null;
if (definitions.length === 0) return null;
return (
<Box sx={{ mt: 2, mb: 2, p: 2, border: '1px dashed #ccc', borderRadius: 1 }}>
<Typography variant="subtitle1" gutterBottom color="primary">
{t("customFields.sectionTitle")}
</Typography>
<Box display="flex" flexWrap="wrap" gap={2}>
{definitions.map(def => (
<Box key={def.id} flexBasis={{ xs: '100%', sm: '48%', md: '32%' }} flexGrow={1}>
<FieldRenderer
definition={def}
value={values?.[def.fieldName]}
onChange={(val) => onChange(def.fieldName, val)}
readOnly={readOnly}
/>
</Box>
))}
</Box>
</Box>
);
};
const FieldRenderer: React.FC<{
definition: CustomFieldDefinition;
value: any;
onChange: (value: any) => void;
readOnly: boolean;
}> = ({ definition, value, onChange, readOnly }) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
};
switch (definition.type) {
case CustomFieldType.Text:
case CustomFieldType.Email:
case CustomFieldType.Url:
return (
<TextField
fullWidth
label={definition.label}
value={value || ''}
onChange={handleChange}
required={definition.isRequired}
disabled={readOnly}
placeholder={definition.placeholder}
helperText={definition.description}
size="small"
/>
);
case CustomFieldType.Number:
return (
<TextField
fullWidth
type="number"
label={definition.label}
value={value || ''}
onChange={handleChange}
required={definition.isRequired}
disabled={readOnly}
helperText={definition.description}
size="small"
/>
);
case CustomFieldType.TextArea:
return (
<TextField
fullWidth
multiline
rows={4}
label={definition.label}
value={value || ''}
onChange={handleChange}
required={definition.isRequired}
disabled={readOnly}
helperText={definition.description}
size="small"
/>
);
case CustomFieldType.Boolean:
return (
<FormControlLabel
control={
<Checkbox
checked={!!value}
onChange={(e) => onChange(e.target.checked)}
disabled={readOnly}
/>
}
label={definition.label}
/>
);
case CustomFieldType.Date:
return (
<DatePicker
label={definition.label}
value={value ? dayjs(value) : null}
onChange={(newValue) => onChange(newValue ? newValue.toISOString() : null)}
disabled={readOnly}
slotProps={{ textField: { fullWidth: true, required: definition.isRequired, helperText: definition.description, size: 'small' } }}
/>
);
case CustomFieldType.Select:
let options: string[] = [];
try {
options = definition.optionsJson ? JSON.parse(definition.optionsJson) : [];
} catch (e) {
console.error("Invalid options JSON", e);
}
return (
<TextField
select
fullWidth
label={definition.label}
value={value || ''}
onChange={handleChange}
required={definition.isRequired}
disabled={readOnly}
helperText={definition.description}
size="small"
>
{options.map((opt) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</TextField>
);
case CustomFieldType.Color:
return (
<TextField
fullWidth
type="color"
label={definition.label}
value={value || '#000000'}
onChange={handleChange}
required={definition.isRequired}
disabled={readOnly}
helperText={definition.description}
InputLabelProps={{ shrink: true }}
size="small"
/>
);
default:
return null;
}
};