130 lines
3.7 KiB
TypeScript
130 lines
3.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
|
|
import { Button } from '@kit/ui/button';
|
|
|
|
import type { CmsFieldType } from '../schema/module.schema';
|
|
import { FieldRenderer } from './field-renderer';
|
|
|
|
interface FieldDefinition {
|
|
name: string;
|
|
display_name: string;
|
|
field_type: CmsFieldType;
|
|
is_required: boolean;
|
|
placeholder?: string | null;
|
|
help_text?: string | null;
|
|
is_readonly: boolean;
|
|
select_options?: Array<{ label: string; value: string }> | null;
|
|
section: string;
|
|
sort_order: number;
|
|
show_in_form: boolean;
|
|
width: string;
|
|
}
|
|
|
|
interface ModuleFormProps {
|
|
fields: FieldDefinition[];
|
|
initialData?: Record<string, unknown>;
|
|
onSubmit: (data: Record<string, unknown>) => Promise<void>;
|
|
isLoading?: boolean;
|
|
errors?: Array<{ field: string; message: string }>;
|
|
}
|
|
|
|
/**
|
|
* Dynamic form component driven by module field definitions.
|
|
* Replaces the legacy my_modulklasse form rendering.
|
|
*/
|
|
export function ModuleForm({
|
|
fields,
|
|
initialData = {},
|
|
onSubmit,
|
|
isLoading = false,
|
|
errors = [],
|
|
}: ModuleFormProps) {
|
|
const [formData, setFormData] =
|
|
useState<Record<string, unknown>>(initialData);
|
|
|
|
const visibleFields = fields
|
|
.filter((f) => f.show_in_form)
|
|
.sort((a, b) => a.sort_order - b.sort_order);
|
|
|
|
// Group fields by section
|
|
const sections = new Map<string, FieldDefinition[]>();
|
|
for (const field of visibleFields) {
|
|
const section = field.section || 'default';
|
|
if (!sections.has(section)) {
|
|
sections.set(section, []);
|
|
}
|
|
sections.get(section)!.push(field);
|
|
}
|
|
|
|
const handleFieldChange = (fieldName: string, value: unknown) => {
|
|
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
await onSubmit(formData);
|
|
};
|
|
|
|
const getFieldError = (fieldName: string) => {
|
|
return errors.find((e) => e.field === fieldName)?.message;
|
|
};
|
|
|
|
const getWidthClass = (width: string) => {
|
|
switch (width) {
|
|
case 'half':
|
|
return 'col-span-1';
|
|
case 'third':
|
|
return 'col-span-1';
|
|
case 'quarter':
|
|
return 'col-span-1';
|
|
default:
|
|
return 'col-span-full';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{Array.from(sections.entries()).map(([sectionName, sectionFields]) => (
|
|
<div key={sectionName} className="space-y-4">
|
|
{sectionName !== 'default' && (
|
|
<h3 className="border-b pb-2 text-lg font-semibold">
|
|
{sectionName}
|
|
</h3>
|
|
)}
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
{sectionFields.map((field) => (
|
|
<div key={field.name} className={getWidthClass(field.width)}>
|
|
<FieldRenderer
|
|
name={field.name}
|
|
displayName={field.display_name}
|
|
fieldType={field.field_type}
|
|
value={formData[field.name]}
|
|
onChange={(value) => handleFieldChange(field.name, value)}
|
|
placeholder={field.placeholder ?? undefined}
|
|
helpText={field.help_text ?? undefined}
|
|
required={field.is_required}
|
|
readonly={field.is_readonly}
|
|
error={getFieldError(field.name)}
|
|
selectOptions={field.select_options ?? undefined}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
<div className="flex justify-end gap-2 border-t pt-4">
|
|
<Button
|
|
type="submit"
|
|
disabled={isLoading}
|
|
data-test="module-record-submit-btn"
|
|
>
|
|
{isLoading ? 'Wird gespeichert...' : 'Speichern'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|