|
|
|
|
@ -1,411 +1,542 @@
|
|
|
|
|
import {
|
|
|
|
|
Box, Typography, Button, Paper, TextField, FormControlLabel,
|
|
|
|
|
Checkbox, Select, MenuItem, FormControl, InputLabel, Divider, CircularProgress
|
|
|
|
|
Box,
|
|
|
|
|
Button,
|
|
|
|
|
Checkbox,
|
|
|
|
|
CircularProgress,
|
|
|
|
|
Divider,
|
|
|
|
|
FormControl,
|
|
|
|
|
FormControlLabel,
|
|
|
|
|
InputLabel,
|
|
|
|
|
MenuItem,
|
|
|
|
|
Paper,
|
|
|
|
|
Select,
|
|
|
|
|
TextField,
|
|
|
|
|
Typography,
|
|
|
|
|
} from "@mui/material";
|
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
|
import MuiAlert from "@mui/material/Alert";
|
|
|
|
|
import Snackbar from "@mui/material/Snackbar";
|
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
import type { Kennzahl } from "../types/kpi";
|
|
|
|
|
import { typeDisplayMapping } from "../types/kpi";
|
|
|
|
|
import Snackbar from "@mui/material/Snackbar";
|
|
|
|
|
import MuiAlert from "@mui/material/Alert";
|
|
|
|
|
|
|
|
|
|
import { API_HOST } from "../util/api";
|
|
|
|
|
|
|
|
|
|
interface KPIFormProps {
|
|
|
|
|
mode: 'add' | 'edit';
|
|
|
|
|
initialData?: Kennzahl | null;
|
|
|
|
|
onSave: (data: Partial<Kennzahl>) => Promise<void>;
|
|
|
|
|
onCancel: () => void;
|
|
|
|
|
loading?: boolean;
|
|
|
|
|
resetTrigger?: number;
|
|
|
|
|
mode: "add" | "edit";
|
|
|
|
|
initialData?: Kennzahl | null;
|
|
|
|
|
onSave: (data: Partial<Kennzahl>) => Promise<void>;
|
|
|
|
|
onCancel: () => void;
|
|
|
|
|
loading?: boolean;
|
|
|
|
|
resetTrigger?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const emptyKPI: Partial<Kennzahl> = {
|
|
|
|
|
name: '',
|
|
|
|
|
mandatory: false,
|
|
|
|
|
type: 'string',
|
|
|
|
|
active: true,
|
|
|
|
|
examples: [{ sentence: '', value: '' }],
|
|
|
|
|
};
|
|
|
|
|
const createEmptyKPI = (): Partial<Kennzahl> => ({
|
|
|
|
|
name: "",
|
|
|
|
|
mandatory: false,
|
|
|
|
|
type: "string",
|
|
|
|
|
active: true,
|
|
|
|
|
examples: [{ sentence: "", value: "" }],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export function KPIForm({
|
|
|
|
|
mode,
|
|
|
|
|
initialData,
|
|
|
|
|
onSave,
|
|
|
|
|
onCancel,
|
|
|
|
|
loading = false,
|
|
|
|
|
}: KPIFormProps) {
|
|
|
|
|
const [formData, setFormData] = useState<Partial<Kennzahl>>(createEmptyKPI());
|
|
|
|
|
const [originalExamples, setOriginalExamples] = useState<
|
|
|
|
|
Array<{ sentence: string; value: string }>
|
|
|
|
|
>([]);
|
|
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
|
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
|
|
|
|
const [snackbarMessage, setSnackbarMessage] = useState("");
|
|
|
|
|
const [snackbarSeverity, setSnackbarSeverity] = useState<
|
|
|
|
|
"success" | "error" | "info"
|
|
|
|
|
>("success");
|
|
|
|
|
|
|
|
|
|
export function KPIForm({ mode, initialData, onSave, onCancel, loading = false, resetTrigger }: KPIFormProps) {
|
|
|
|
|
const [formData, setFormData] = useState<Partial<Kennzahl>>(emptyKPI);
|
|
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
|
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
|
|
|
|
const [snackbarMessage, setSnackbarMessage] = useState("");
|
|
|
|
|
const [snackbarSeverity, setSnackbarSeverity] = useState<'success' | 'error' | 'info'>("success");
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (mode === "edit" && initialData) {
|
|
|
|
|
setOriginalExamples(initialData.examples || []);
|
|
|
|
|
setFormData({
|
|
|
|
|
...initialData,
|
|
|
|
|
examples: [{ sentence: "", value: "" }],
|
|
|
|
|
});
|
|
|
|
|
} else if (mode === "add") {
|
|
|
|
|
setOriginalExamples([]);
|
|
|
|
|
setFormData(createEmptyKPI());
|
|
|
|
|
}
|
|
|
|
|
}, [mode, initialData]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (mode === 'edit' && initialData) {
|
|
|
|
|
setFormData(initialData);
|
|
|
|
|
} else if (mode === 'add') {
|
|
|
|
|
setFormData(emptyKPI);
|
|
|
|
|
}
|
|
|
|
|
}, [mode, initialData]);
|
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
if (!formData.name?.trim()) {
|
|
|
|
|
setSnackbarMessage("Name ist erforderlich");
|
|
|
|
|
setSnackbarSeverity("error");
|
|
|
|
|
setSnackbarOpen(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (mode === 'add') {
|
|
|
|
|
setFormData(emptyKPI);
|
|
|
|
|
}
|
|
|
|
|
}, [resetTrigger]);
|
|
|
|
|
if (mode === "add") {
|
|
|
|
|
if (!formData.examples || formData.examples.length === 0) {
|
|
|
|
|
setSnackbarMessage("Mindestens ein Beispielsatz ist erforderlich");
|
|
|
|
|
setSnackbarSeverity("error");
|
|
|
|
|
setSnackbarOpen(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newExamples = formData.examples.filter(
|
|
|
|
|
(ex) => ex.sentence?.trim() && ex.value?.trim(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (newExamples.length === 0) {
|
|
|
|
|
setSnackbarMessage(
|
|
|
|
|
"Mindestens ein vollständiger Beispielsatz ist erforderlich.",
|
|
|
|
|
);
|
|
|
|
|
setSnackbarSeverity("error");
|
|
|
|
|
setSnackbarOpen(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
if (!formData.name?.trim()) {
|
|
|
|
|
setSnackbarMessage("Name ist erforderlich");
|
|
|
|
|
setSnackbarSeverity("error");
|
|
|
|
|
setSnackbarOpen(true);
|
|
|
|
|
const newExamples = (formData.examples || []).filter(
|
|
|
|
|
(ex) => ex.sentence?.trim() && ex.value?.trim(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (formData.examples && formData.examples.length > 0) {
|
|
|
|
|
for (const ex of formData.examples) {
|
|
|
|
|
if (!ex.sentence?.trim() && !ex.value?.trim()) continue;
|
|
|
|
|
if (!ex.sentence?.trim() || !ex.value?.trim()) {
|
|
|
|
|
setSnackbarMessage("Alle Beispielsätze müssen vollständig sein oder leer gelassen werden.");
|
|
|
|
|
setSnackbarSeverity("error");
|
|
|
|
|
setSnackbarOpen(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!formData.examples || formData.examples.length === 0) {
|
|
|
|
|
setSnackbarMessage("Mindestens ein Beispielsatz ist erforderlich");
|
|
|
|
|
setSnackbarSeverity("error");
|
|
|
|
|
setSnackbarOpen(true);
|
|
|
|
|
setIsSaving(true);
|
|
|
|
|
try {
|
|
|
|
|
if (newExamples.length > 0) {
|
|
|
|
|
const spacyEntries = generateSpacyEntries({
|
|
|
|
|
...formData,
|
|
|
|
|
examples: newExamples,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Für jeden einzelnen Beispielsatz:
|
|
|
|
|
for (const entry of spacyEntries) {
|
|
|
|
|
// im localStorage speichern (zum Debuggen oder Vorschau)
|
|
|
|
|
const stored = localStorage.getItem("spacyData");
|
|
|
|
|
const existingData = stored ? JSON.parse(stored) : [];
|
|
|
|
|
const updated = [...existingData, entry];
|
|
|
|
|
localStorage.setItem("spacyData", JSON.stringify(updated));
|
|
|
|
|
|
|
|
|
|
for (const ex of formData.examples) {
|
|
|
|
|
if (!ex.sentence?.trim() || !ex.value?.trim()) {
|
|
|
|
|
setSnackbarMessage('Alle Beispielsätze müssen vollständig sein.');
|
|
|
|
|
setSnackbarSeverity("error");
|
|
|
|
|
setSnackbarOpen(true);
|
|
|
|
|
// POST Request an das Flask-Backend
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`${API_HOST}/api/spacy/append-training-entry`,
|
|
|
|
|
{
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(entry),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
data.error || "Fehler beim Aufruf von append-training-entry",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsSaving(true);
|
|
|
|
|
try {
|
|
|
|
|
const spacyEntries = generateSpacyEntries(formData);
|
|
|
|
|
console.log("SpaCy-Eintrag gespeichert:", data);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Für jeden einzelnen Beispielsatz:
|
|
|
|
|
for (const entry of spacyEntries) {
|
|
|
|
|
// im localStorage speichern (zum Debuggen oder Vorschau)
|
|
|
|
|
const stored = localStorage.getItem("spacyData");
|
|
|
|
|
const existingData = stored ? JSON.parse(stored) : [];
|
|
|
|
|
const updated = [...existingData, entry];
|
|
|
|
|
localStorage.setItem("spacyData", JSON.stringify(updated));
|
|
|
|
|
const allExamples =
|
|
|
|
|
mode === "edit" ? [...originalExamples, ...newExamples] : newExamples;
|
|
|
|
|
|
|
|
|
|
// POST Request an das Flask-Backend
|
|
|
|
|
const response = await fetch("http://localhost:5050/api/spacy/append-training-entry", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json"
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(entry)
|
|
|
|
|
});
|
|
|
|
|
// Dann in die DB speichern
|
|
|
|
|
await onSave({
|
|
|
|
|
name: formData.name ?? "",
|
|
|
|
|
mandatory: formData.mandatory ?? false,
|
|
|
|
|
type: formData.type || "string",
|
|
|
|
|
position: formData.position ?? 0,
|
|
|
|
|
active: formData.active ?? true,
|
|
|
|
|
examples: allExamples,
|
|
|
|
|
is_trained: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
// Formular zurücksetzen:
|
|
|
|
|
if (mode === "add") {
|
|
|
|
|
setFormData(createEmptyKPI());
|
|
|
|
|
} else {
|
|
|
|
|
setFormData((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
examples: [{ sentence: "", value: "" }],
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(data.error || "Fehler beim Aufruf von append-training-entry");
|
|
|
|
|
}
|
|
|
|
|
const successMessage = newExamples.length > 0
|
|
|
|
|
? "Beispielsätze gespeichert. Jetzt auf -Neu trainieren- klicken oder weitere Kennzahlen hinzufügen."
|
|
|
|
|
: mode === "edit"
|
|
|
|
|
? "Kennzahl erfolgreich aktualisiert."
|
|
|
|
|
: "Kennzahl erfolgreich erstellt.";
|
|
|
|
|
|
|
|
|
|
console.log("SpaCy-Eintrag gespeichert:", data);
|
|
|
|
|
}
|
|
|
|
|
setSnackbarMessage(successMessage);
|
|
|
|
|
setSnackbarSeverity("success");
|
|
|
|
|
setSnackbarOpen(true);
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
// Prüfe auf 409-Fehler
|
|
|
|
|
if (e?.message?.includes("409") || e?.response?.status === 409) {
|
|
|
|
|
setSnackbarMessage(
|
|
|
|
|
"Diese Kennzahl existiert bereits. Sie können sie unter -Konfiguration- bearbeiten.",
|
|
|
|
|
);
|
|
|
|
|
setSnackbarSeverity("info");
|
|
|
|
|
setSnackbarOpen(true);
|
|
|
|
|
} else {
|
|
|
|
|
setSnackbarMessage(e.message || "Fehler beim Speichern.");
|
|
|
|
|
setSnackbarSeverity("error");
|
|
|
|
|
setSnackbarOpen(true);
|
|
|
|
|
}
|
|
|
|
|
console.error(e);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Dann in die DB speichern
|
|
|
|
|
await onSave({
|
|
|
|
|
name: formData.name!,
|
|
|
|
|
mandatory: formData.mandatory ?? false,
|
|
|
|
|
type: formData.type || 'string',
|
|
|
|
|
position: formData.position ?? 0,
|
|
|
|
|
active: formData.active ?? true,
|
|
|
|
|
examples: formData.examples ?? [],
|
|
|
|
|
is_trained: false,
|
|
|
|
|
});
|
|
|
|
|
// Formular zurücksetzen:
|
|
|
|
|
setFormData(emptyKPI);
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
setFormData(createEmptyKPI());
|
|
|
|
|
onCancel();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateField = (field: keyof Kennzahl, value: any) => {
|
|
|
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
setSnackbarMessage("Beispielsätze gespeichert. Jetzt auf -Neu trainieren- klicken oder weitere Kennzahlen hinzufügen.");
|
|
|
|
|
setSnackbarSeverity("success");
|
|
|
|
|
setSnackbarOpen(true);
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
// Prüfe auf 409-Fehler
|
|
|
|
|
if (e?.message?.includes("409") || e?.response?.status === 409) {
|
|
|
|
|
setSnackbarMessage("Diese Kennzahl existiert bereits. Sie können sie unter -Konfiguration- bearbeiten.");
|
|
|
|
|
setSnackbarSeverity("info");
|
|
|
|
|
setSnackbarOpen(true);
|
|
|
|
|
} else {
|
|
|
|
|
setSnackbarMessage(e.message || "Fehler beim Speichern.");
|
|
|
|
|
setSnackbarSeverity("error");
|
|
|
|
|
setSnackbarOpen(true);
|
|
|
|
|
}
|
|
|
|
|
console.error(e);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
}
|
|
|
|
|
const updateExample = (
|
|
|
|
|
index: number,
|
|
|
|
|
field: "sentence" | "value",
|
|
|
|
|
value: string,
|
|
|
|
|
) => {
|
|
|
|
|
const newExamples = [...(formData.examples || [])];
|
|
|
|
|
newExamples[index][field] = value;
|
|
|
|
|
updateField("examples", newExamples);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
const addExample = () => {
|
|
|
|
|
const newExamples = [
|
|
|
|
|
...(formData.examples || []),
|
|
|
|
|
{ sentence: "", value: "" },
|
|
|
|
|
];
|
|
|
|
|
updateField("examples", newExamples);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const removeExample = (index: number) => {
|
|
|
|
|
const newExamples = [...(formData.examples || [])];
|
|
|
|
|
newExamples.splice(index, 1);
|
|
|
|
|
updateField("examples", newExamples);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
setFormData(emptyKPI);
|
|
|
|
|
onCancel();
|
|
|
|
|
};
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<Box
|
|
|
|
|
display="flex"
|
|
|
|
|
justifyContent="center"
|
|
|
|
|
alignItems="center"
|
|
|
|
|
minHeight="400px"
|
|
|
|
|
flexDirection="column"
|
|
|
|
|
>
|
|
|
|
|
<CircularProgress sx={{ color: "#383838", mb: 2 }} />
|
|
|
|
|
<Typography>
|
|
|
|
|
{mode === "edit" ? "Lade KPI Details..." : "Laden..."}
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updateField = (field: keyof Kennzahl, value: any) => {
|
|
|
|
|
setFormData(prev => ({ ...prev, [field]: value }));
|
|
|
|
|
};
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Paper
|
|
|
|
|
elevation={2}
|
|
|
|
|
sx={{
|
|
|
|
|
width: "90%",
|
|
|
|
|
maxWidth: 800,
|
|
|
|
|
p: 4,
|
|
|
|
|
borderRadius: 2,
|
|
|
|
|
backgroundColor: "white",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Box mb={4}>
|
|
|
|
|
<Typography variant="h6" fontWeight="bold" mb={2}>
|
|
|
|
|
Kennzahl
|
|
|
|
|
</Typography>
|
|
|
|
|
<TextField
|
|
|
|
|
fullWidth
|
|
|
|
|
label="Name *"
|
|
|
|
|
placeholder="z.B. IRR"
|
|
|
|
|
value={formData.name || ""}
|
|
|
|
|
onChange={(e) => updateField("name", e.target.value)}
|
|
|
|
|
sx={{ mb: 2 }}
|
|
|
|
|
required
|
|
|
|
|
error={!formData.name?.trim()}
|
|
|
|
|
helperText={!formData.name?.trim() ? "Name ist erforderlich" : ""}
|
|
|
|
|
/>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
const updateExample = (index: number, field: 'sentence' | 'value', value: string) => {
|
|
|
|
|
const newExamples = [...(formData.examples || [])];
|
|
|
|
|
newExamples[index][field] = value;
|
|
|
|
|
updateField('examples', newExamples);
|
|
|
|
|
};
|
|
|
|
|
<Divider sx={{ my: 3 }} />
|
|
|
|
|
|
|
|
|
|
const addExample = () => {
|
|
|
|
|
const newExamples = [...(formData.examples || []), { sentence: '', value: '' }];
|
|
|
|
|
updateField('examples', newExamples);
|
|
|
|
|
};
|
|
|
|
|
<Box mb={4}>
|
|
|
|
|
<Typography variant="h6" fontWeight="bold" mb={2}>
|
|
|
|
|
Format:{" "}
|
|
|
|
|
{typeDisplayMapping[
|
|
|
|
|
formData.type as keyof typeof typeDisplayMapping
|
|
|
|
|
] || formData.type}
|
|
|
|
|
</Typography>
|
|
|
|
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
|
|
|
|
<InputLabel>Typ</InputLabel>
|
|
|
|
|
<Select
|
|
|
|
|
value={formData.type || "string"}
|
|
|
|
|
label="Typ"
|
|
|
|
|
onChange={(e) => updateField("type", e.target.value)}
|
|
|
|
|
>
|
|
|
|
|
<MenuItem value="string">Text</MenuItem>
|
|
|
|
|
<MenuItem value="number">Zahl</MenuItem>
|
|
|
|
|
<MenuItem value="date">Datum</MenuItem>
|
|
|
|
|
<MenuItem value="boolean">Ja/Nein</MenuItem>
|
|
|
|
|
<MenuItem value="array">Liste (mehrfach)</MenuItem>
|
|
|
|
|
</Select>
|
|
|
|
|
</FormControl>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
const removeExample = (index: number) => {
|
|
|
|
|
const newExamples = [...(formData.examples || [])];
|
|
|
|
|
newExamples.splice(index, 1);
|
|
|
|
|
updateField('examples', newExamples);
|
|
|
|
|
};
|
|
|
|
|
<Divider sx={{ my: 3 }} />
|
|
|
|
|
|
|
|
|
|
<Box mb={4}>
|
|
|
|
|
<FormControlLabel
|
|
|
|
|
control={
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={formData.active !== false}
|
|
|
|
|
onChange={(e) => updateField("active", e.target.checked)}
|
|
|
|
|
sx={{
|
|
|
|
|
color: "#666666",
|
|
|
|
|
"&.Mui-checked": {
|
|
|
|
|
color: "#333333",
|
|
|
|
|
},
|
|
|
|
|
"&:hover": {
|
|
|
|
|
backgroundColor: "rgba(102, 102, 102, 0.04)",
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
label="Aktiv"
|
|
|
|
|
/>
|
|
|
|
|
<Typography variant="body2" color="text.secondary" ml={4}>
|
|
|
|
|
Die Kennzahl ist aktiv und wird angezeigt
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
<Box mt={3}>
|
|
|
|
|
<FormControlLabel
|
|
|
|
|
control={
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={formData.mandatory || false}
|
|
|
|
|
onChange={(e) => updateField("mandatory", e.target.checked)}
|
|
|
|
|
sx={{
|
|
|
|
|
color: "#666666",
|
|
|
|
|
"&.Mui-checked": {
|
|
|
|
|
color: "#333333",
|
|
|
|
|
},
|
|
|
|
|
"&:hover": {
|
|
|
|
|
backgroundColor: "rgba(102, 102, 102, 0.04)",
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
label="Erforderlich"
|
|
|
|
|
/>
|
|
|
|
|
<Typography variant="body2" color="text.secondary" ml={4}>
|
|
|
|
|
Die Kennzahl erlaubt keine leeren Werte
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<Box
|
|
|
|
|
display="flex"
|
|
|
|
|
justifyContent="center"
|
|
|
|
|
alignItems="center"
|
|
|
|
|
minHeight="400px"
|
|
|
|
|
flexDirection="column"
|
|
|
|
|
>
|
|
|
|
|
<CircularProgress sx={{ color: '#383838', mb: 2 }} />
|
|
|
|
|
<Typography>
|
|
|
|
|
{mode === 'edit' ? 'Lade KPI Details...' : 'Laden...'}
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
<Divider sx={{ my: 3 }} />
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Paper
|
|
|
|
|
elevation={2}
|
|
|
|
|
sx={{
|
|
|
|
|
width: "90%",
|
|
|
|
|
maxWidth: 800,
|
|
|
|
|
p: 4,
|
|
|
|
|
borderRadius: 2,
|
|
|
|
|
backgroundColor: "white"
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Box mb={4}>
|
|
|
|
|
<Typography variant="h6" fontWeight="bold" mb={2}>
|
|
|
|
|
Kennzahl
|
|
|
|
|
</Typography>
|
|
|
|
|
<TextField
|
|
|
|
|
fullWidth
|
|
|
|
|
label="Name *"
|
|
|
|
|
placeholder="z.B. IRR"
|
|
|
|
|
value={formData.name || ''}
|
|
|
|
|
onChange={(e) => updateField('name', e.target.value)}
|
|
|
|
|
sx={{ mb: 2 }}
|
|
|
|
|
required
|
|
|
|
|
error={!formData.name?.trim()}
|
|
|
|
|
helperText={!formData.name?.trim() ? 'Name ist erforderlich' : ''}
|
|
|
|
|
/>
|
|
|
|
|
</Box>
|
|
|
|
|
{/* Hinweistext wie viele Beispielsätzen vorhanden sind*/}
|
|
|
|
|
{mode === "edit" && originalExamples.length > 0 && (
|
|
|
|
|
<Box
|
|
|
|
|
mb={2}
|
|
|
|
|
p={2}
|
|
|
|
|
sx={{
|
|
|
|
|
backgroundColor: "#e3f2fd",
|
|
|
|
|
border: "1px solid #90caf9",
|
|
|
|
|
borderRadius: 2,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Typography variant="body1" sx={{ fontWeight: "bold", mb: 1 }}>
|
|
|
|
|
Vorhandene Beispielsätze: {originalExamples.length}
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography variant="body2">
|
|
|
|
|
Diese Kennzahl hat bereits {originalExamples.length}{" "}
|
|
|
|
|
Beispielsätze. Neue Beispielsätze werden zu den vorhandenen
|
|
|
|
|
hinzugefügt.
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Divider sx={{ my: 3 }} />
|
|
|
|
|
{/* Hinweistext vor Beispielsätzen */}
|
|
|
|
|
<Box
|
|
|
|
|
mb={2}
|
|
|
|
|
p={2}
|
|
|
|
|
sx={{
|
|
|
|
|
backgroundColor: "#fff8e1",
|
|
|
|
|
border: "1px solid #ffe082",
|
|
|
|
|
borderRadius: 2,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Typography variant="body1" sx={{ fontWeight: "bold", mb: 1 }}>
|
|
|
|
|
Hinweis zur Trainingsqualität
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography variant="body2">
|
|
|
|
|
Damit das System neue Kennzahlen zuverlässig erkennen kann,
|
|
|
|
|
empfehlen wir <strong>mindestens 5 Beispielsätze</strong> zu
|
|
|
|
|
erstellen – je mehr, desto besser.
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography variant="body2" mt={1}>
|
|
|
|
|
<strong>Wichtig:</strong> Neue Kennzahlen werden erst in
|
|
|
|
|
PDF-Dokumenten erkannt, wenn Sie den Button{" "}
|
|
|
|
|
<em>"Neu trainieren"</em> auf der Konfigurationsseite ausführen.
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography variant="body2" mt={1}>
|
|
|
|
|
<strong>Tipp:</strong> Sie können jederzeit weitere Beispielsätze
|
|
|
|
|
hinzufügen.
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
<Box mb={4}>
|
|
|
|
|
<Typography variant="h6" fontWeight="bold" mb={2}>
|
|
|
|
|
Format: {typeDisplayMapping[formData.type as keyof typeof typeDisplayMapping] || formData.type}
|
|
|
|
|
</Typography>
|
|
|
|
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
|
|
|
|
<InputLabel>Typ</InputLabel>
|
|
|
|
|
<Select
|
|
|
|
|
value={formData.type || 'string'}
|
|
|
|
|
label="Typ"
|
|
|
|
|
onChange={(e) => updateField('type', e.target.value)}
|
|
|
|
|
>
|
|
|
|
|
<MenuItem value="string">Text</MenuItem>
|
|
|
|
|
<MenuItem value="number">Zahl</MenuItem>
|
|
|
|
|
<MenuItem value="date">Datum</MenuItem>
|
|
|
|
|
<MenuItem value="boolean">Ja/Nein</MenuItem>
|
|
|
|
|
<MenuItem value="array">Liste (mehrfach)</MenuItem>
|
|
|
|
|
</Select>
|
|
|
|
|
</FormControl>
|
|
|
|
|
</Box>
|
|
|
|
|
<Box mb={4}>
|
|
|
|
|
<Typography variant="h6" fontWeight="bold" mb={2}>
|
|
|
|
|
Beispielsätze
|
|
|
|
|
</Typography>
|
|
|
|
|
{(formData.examples || []).map((ex, idx) => (
|
|
|
|
|
<Box
|
|
|
|
|
key={idx}
|
|
|
|
|
sx={{ mb: 2, border: "1px solid #ccc", p: 2, borderRadius: 1 }}
|
|
|
|
|
>
|
|
|
|
|
<TextField
|
|
|
|
|
fullWidth
|
|
|
|
|
multiline
|
|
|
|
|
label={`Beispielsatz ${idx + 1}`}
|
|
|
|
|
placeholder="z.B. Die IRR beträgt 7,8 %"
|
|
|
|
|
value={ex.sentence}
|
|
|
|
|
onChange={(e) => updateExample(idx, "sentence", e.target.value)}
|
|
|
|
|
required
|
|
|
|
|
sx={{ mb: 1 }}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<TextField
|
|
|
|
|
fullWidth
|
|
|
|
|
label="Bezeichneter Wert im Satz"
|
|
|
|
|
placeholder="z.B. 7,8 %"
|
|
|
|
|
value={ex.value}
|
|
|
|
|
onChange={(e) => updateExample(idx, "value", e.target.value)}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
{(formData.examples?.length || 0) > 1 && (
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => removeExample(idx)}
|
|
|
|
|
sx={{ mt: 1 }}
|
|
|
|
|
color="error"
|
|
|
|
|
>
|
|
|
|
|
Entfernen
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
{mode === 'add' && (
|
|
|
|
|
<>
|
|
|
|
|
<Divider sx={{ my: 3 }} />
|
|
|
|
|
<Box mb={4}>
|
|
|
|
|
<FormControlLabel
|
|
|
|
|
control={
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={formData.active !== false}
|
|
|
|
|
onChange={(e) => updateField('active', e.target.checked)}
|
|
|
|
|
sx={{ color: '#383838' }}
|
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
label="Aktiv"
|
|
|
|
|
/>
|
|
|
|
|
<Typography variant="body2" color="text.secondary" ml={4}>
|
|
|
|
|
Die Kennzahl ist aktiv und wird angezeigt
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
<Box mt={3}>
|
|
|
|
|
<FormControlLabel
|
|
|
|
|
control={
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={formData.mandatory || false}
|
|
|
|
|
onChange={(e) => updateField('mandatory', e.target.checked)}
|
|
|
|
|
sx={{ color: '#383838' }}
|
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
label="Erforderlich"
|
|
|
|
|
/>
|
|
|
|
|
<Typography variant="body2" color="text.secondary" ml={4}>
|
|
|
|
|
Die Kennzahl erlaubt keine leeren Werte
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
<Button variant="outlined" onClick={addExample}>
|
|
|
|
|
+ Beispielsatz hinzufügen
|
|
|
|
|
</Button>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
<Divider sx={{ my: 3 }} />
|
|
|
|
|
|
|
|
|
|
{/* Hinweistext vor Beispielsätzen */}
|
|
|
|
|
<Box mb={2} p={2} sx={{ backgroundColor: '#fff8e1', border: '1px solid #ffe082', borderRadius: 2 }}>
|
|
|
|
|
<Typography variant="body1" sx={{ fontWeight: 'bold', mb: 1 }}>
|
|
|
|
|
Hinweis zur Trainingsqualität
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography variant="body2">
|
|
|
|
|
Damit das System neue Kennzahlen zuverlässig erkennen kann, empfehlen wir <strong>mindestens 5 Beispielsätze</strong> zu erstellen – je mehr, desto besser.
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography variant="body2" mt={1}>
|
|
|
|
|
<strong>Wichtig:</strong> Neue Kennzahlen werden erst in PDF-Dokumenten erkannt, wenn Sie den Button <em>"Neu trainieren"</em> auf der Konfigurationsseite ausführen.
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography variant="body2" mt={1}>
|
|
|
|
|
<strong>Tipp:</strong> Sie können jederzeit weitere Beispielsätze hinzufügen oder vorhandene in der Kennzahlenverwaltung bearbeiten.
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
<Box mb={4}>
|
|
|
|
|
|
|
|
|
|
<Typography variant="h6" fontWeight="bold" mb={2}>
|
|
|
|
|
Beispielsätze
|
|
|
|
|
</Typography>
|
|
|
|
|
{(formData.examples || []).map((ex, idx) => (
|
|
|
|
|
<Box key={idx} sx={{ mb: 2, border: '1px solid #ccc', p: 2, borderRadius: 1 }}>
|
|
|
|
|
<TextField
|
|
|
|
|
fullWidth
|
|
|
|
|
multiline
|
|
|
|
|
label={`Beispielsatz ${idx + 1}`}
|
|
|
|
|
placeholder="z.B. Die IRR beträgt 7,8 %"
|
|
|
|
|
value={ex.sentence}
|
|
|
|
|
onChange={(e) => updateExample(idx, 'sentence', e.target.value)}
|
|
|
|
|
required
|
|
|
|
|
sx={{ mb: 1 }}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<TextField
|
|
|
|
|
fullWidth
|
|
|
|
|
label="Bezeichneter Wert im Satz"
|
|
|
|
|
placeholder="z.B. 7,8 %"
|
|
|
|
|
value={ex.value}
|
|
|
|
|
onChange={(e) => updateExample(idx, 'value', e.target.value)}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
{(formData.examples?.length || 0) > 1 && (
|
|
|
|
|
<Button onClick={() => removeExample(idx)} sx={{ mt: 1 }} color="error">
|
|
|
|
|
Entfernen
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
<Button variant="outlined" onClick={addExample}>
|
|
|
|
|
+ Beispielsatz hinzufügen
|
|
|
|
|
</Button>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
<Box display="flex" justifyContent="flex-end" gap={2} mt={4}>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outlined"
|
|
|
|
|
onClick={handleCancel}
|
|
|
|
|
disabled={isSaving}
|
|
|
|
|
sx={{
|
|
|
|
|
borderColor: "#383838",
|
|
|
|
|
color: "#383838",
|
|
|
|
|
"&:hover": { borderColor: "#2e2e2e", backgroundColor: "#f5f5f5" }
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Abbrechen
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="contained"
|
|
|
|
|
onClick={handleSave}
|
|
|
|
|
disabled={isSaving || !formData.name?.trim()}
|
|
|
|
|
sx={{
|
|
|
|
|
backgroundColor: "#383838",
|
|
|
|
|
"&:hover": { backgroundColor: "#2e2e2e" },
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{isSaving ? (
|
|
|
|
|
<>
|
|
|
|
|
<CircularProgress size={16} sx={{ mr: 1, color: 'white' }} />
|
|
|
|
|
{mode === 'add' ? 'Hinzufügen...' : 'Speichern...'}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
mode === 'add' ? 'Hinzufügen' : 'Speichern'
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</Box>
|
|
|
|
|
</Paper>
|
|
|
|
|
<Snackbar
|
|
|
|
|
open={snackbarOpen}
|
|
|
|
|
autoHideDuration={5000}
|
|
|
|
|
onClose={() => setSnackbarOpen(false)}
|
|
|
|
|
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
|
|
|
|
>
|
|
|
|
|
<MuiAlert
|
|
|
|
|
elevation={6}
|
|
|
|
|
variant="filled"
|
|
|
|
|
onClose={() => setSnackbarOpen(false)}
|
|
|
|
|
severity={snackbarSeverity}
|
|
|
|
|
sx={{ width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
|
|
|
|
|
>
|
|
|
|
|
<span>{snackbarMessage}</span>
|
|
|
|
|
<Button color="inherit" size="small" onClick={() => setSnackbarOpen(false)}>
|
|
|
|
|
OK
|
|
|
|
|
</Button>
|
|
|
|
|
</MuiAlert>
|
|
|
|
|
|
|
|
|
|
</Snackbar>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
<Box display="flex" justifyContent="flex-end" gap={2} mt={4}>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outlined"
|
|
|
|
|
onClick={handleCancel}
|
|
|
|
|
disabled={isSaving}
|
|
|
|
|
sx={{
|
|
|
|
|
borderColor: "#383838",
|
|
|
|
|
color: "#383838",
|
|
|
|
|
"&:hover": { borderColor: "#2e2e2e", backgroundColor: "#f5f5f5" },
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Abbrechen
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="contained"
|
|
|
|
|
onClick={handleSave}
|
|
|
|
|
disabled={isSaving || !formData.name?.trim()}
|
|
|
|
|
sx={{
|
|
|
|
|
backgroundColor: "#383838",
|
|
|
|
|
"&:hover": { backgroundColor: "#2e2e2e" },
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{isSaving ? (
|
|
|
|
|
<>
|
|
|
|
|
<CircularProgress size={16} sx={{ mr: 1, color: "white" }} />
|
|
|
|
|
{mode === "add" ? "Hinzufügen..." : "Speichern..."}
|
|
|
|
|
</>
|
|
|
|
|
) : mode === "add" ? (
|
|
|
|
|
"Hinzufügen"
|
|
|
|
|
) : (
|
|
|
|
|
"Speichern"
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</Box>
|
|
|
|
|
</Paper>
|
|
|
|
|
<Snackbar
|
|
|
|
|
open={snackbarOpen}
|
|
|
|
|
autoHideDuration={5000}
|
|
|
|
|
onClose={() => setSnackbarOpen(false)}
|
|
|
|
|
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
|
|
|
|
>
|
|
|
|
|
<MuiAlert
|
|
|
|
|
elevation={6}
|
|
|
|
|
variant="filled"
|
|
|
|
|
onClose={() => setSnackbarOpen(false)}
|
|
|
|
|
severity={snackbarSeverity}
|
|
|
|
|
sx={{
|
|
|
|
|
width: "100%",
|
|
|
|
|
display: "flex",
|
|
|
|
|
justifyContent: "space-between",
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span>{snackbarMessage}</span>
|
|
|
|
|
<Button
|
|
|
|
|
color="inherit"
|
|
|
|
|
size="small"
|
|
|
|
|
onClick={() => setSnackbarOpen(false)}
|
|
|
|
|
>
|
|
|
|
|
OK
|
|
|
|
|
</Button>
|
|
|
|
|
</MuiAlert>
|
|
|
|
|
</Snackbar>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function generateSpacyEntries(formData: Partial<Kennzahl>) {
|
|
|
|
|
const label = formData.name?.trim().toUpperCase() || "";
|
|
|
|
|
return (formData.examples || []).map(({ sentence, value }) => {
|
|
|
|
|
const trimmedValue = value.trim();
|
|
|
|
|
const start = sentence.indexOf(trimmedValue);
|
|
|
|
|
if (start === -1) {
|
|
|
|
|
throw new Error(`"${trimmedValue}" nicht gefunden in Satz: "${sentence}"`);
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
text: sentence,
|
|
|
|
|
entities: [[start, start + trimmedValue.length, label]]
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
const label = formData.name?.trim().toUpperCase() || "";
|
|
|
|
|
return (formData.examples || []).map(({ sentence, value }) => {
|
|
|
|
|
const trimmedValue = value.trim();
|
|
|
|
|
const start = sentence.indexOf(trimmedValue);
|
|
|
|
|
if (start === -1) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`"${trimmedValue}" nicht gefunden in Satz: "${sentence}"`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
text: sentence,
|
|
|
|
|
entities: [[start, start + trimmedValue.length, label]],
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|