476 lines
13 KiB
TypeScript
476 lines
13 KiB
TypeScript
import type { Kennzahl } from "@/types/kpi";
|
|
import EditIcon from "@mui/icons-material/Edit";
|
|
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
|
import SearchIcon from "@mui/icons-material/Search";
|
|
import {
|
|
Box,
|
|
Link,
|
|
Paper,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
TextField,
|
|
Tooltip,
|
|
} from "@mui/material";
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { useNavigate } from "@tanstack/react-router";
|
|
import { useState } from "react";
|
|
import type { KeyboardEvent } from "react";
|
|
import { fetchPutKPI } from "../util/api";
|
|
|
|
interface KennzahlenTableProps {
|
|
onPageClick?: (page: number, text: string) => void;
|
|
pdfId: string;
|
|
settings: Kennzahl[];
|
|
data: {
|
|
[key: string]: {
|
|
label: string;
|
|
entity: string;
|
|
page: number;
|
|
status: string;
|
|
source: string;
|
|
}[];
|
|
};
|
|
from?: string;
|
|
}
|
|
|
|
export default function KennzahlenTable({
|
|
onPageClick,
|
|
data,
|
|
pdfId,
|
|
settings,
|
|
from,
|
|
}: KennzahlenTableProps) {
|
|
const [editingIndex, setEditingIndex] = useState<string>("");
|
|
const [editValue, setEditValue] = useState("");
|
|
const [editingPageIndex, setEditingPageIndex] = useState<string>("");
|
|
const [editPageValue, setEditPageValue] = useState("");
|
|
const [hoveredPageIndex, setHoveredPageIndex] = useState<string>("");
|
|
const navigate = useNavigate();
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
const { mutate } = useMutation({
|
|
mutationFn: (params: { id: string; newValue?: string; newPage?: number }) => {
|
|
const { id, newValue, newPage } = params;
|
|
const key = id.toUpperCase();
|
|
const updatedData = { ...data };
|
|
|
|
if (data[key] && data[key].length > 0) {
|
|
updatedData[key] = data[key].map((item) => ({
|
|
...item,
|
|
...(newValue !== undefined && { entity: newValue }),
|
|
...(newPage !== undefined && { page: newPage }),
|
|
}));
|
|
} else {
|
|
updatedData[key] = [{
|
|
label: key,
|
|
entity: newValue || "",
|
|
page: newPage || 0,
|
|
status: "single-source",
|
|
source: "manual"
|
|
}];
|
|
}
|
|
|
|
return fetchPutKPI(Number(pdfId), updatedData);
|
|
},
|
|
onMutate: async (params: { id: string; newValue?: string; newPage?: number }) => {
|
|
const { id, newValue, newPage } = params;
|
|
await queryClient.cancelQueries({
|
|
queryKey: ["pitchBookKPI", pdfId],
|
|
});
|
|
|
|
const snapshot = queryClient.getQueryData(["pitchBookKPI", pdfId]);
|
|
|
|
const key = id.toUpperCase();
|
|
|
|
queryClient.setQueryData(["pitchBookKPI", pdfId], () => {
|
|
const updatedData = { ...data };
|
|
|
|
if (data[key] && data[key].length > 0) {
|
|
updatedData[key] = data[key].map((item) => ({
|
|
...item,
|
|
...(newValue !== undefined && { entity: newValue }),
|
|
...(newPage !== undefined && { page: newPage }),
|
|
}));
|
|
} else {
|
|
updatedData[key] = [{
|
|
label: key,
|
|
entity: newValue || "",
|
|
page: newPage || 0,
|
|
status: "single-source",
|
|
source: "manual"
|
|
}];
|
|
}
|
|
|
|
return updatedData;
|
|
});
|
|
|
|
return () => {
|
|
queryClient.setQueryData(["pitchBookKPI", pdfId], snapshot);
|
|
};
|
|
},
|
|
onError: (error, _variables, rollback) => {
|
|
console.log("error", error);
|
|
rollback?.();
|
|
},
|
|
onSettled: () => {
|
|
return queryClient.invalidateQueries({
|
|
queryKey: ["pitchBookKPI", pdfId],
|
|
});
|
|
},
|
|
});
|
|
|
|
// Bearbeitung starten
|
|
const startEditing = (value: string, index: string) => {
|
|
setEditingIndex(index);
|
|
setEditValue(value);
|
|
};
|
|
|
|
const startPageEditing = (value: number, index: string) => {
|
|
setEditingPageIndex(index);
|
|
setEditPageValue(value.toString());
|
|
};
|
|
|
|
// Bearbeitung beenden und Wert speichern
|
|
const handleSave = async (index: string) => {
|
|
mutate({ id: index, newValue: editValue });
|
|
setEditingIndex("");
|
|
};
|
|
|
|
const handlePageSave = async (index: string) => {
|
|
const pageNumber = parseInt(editPageValue);
|
|
if (editPageValue === "" || pageNumber === 0) {
|
|
mutate({ id: index, newPage: 0 });
|
|
} else if (!isNaN(pageNumber) && pageNumber > 0) {
|
|
mutate({ id: index, newPage: pageNumber });
|
|
}
|
|
setEditingPageIndex("");
|
|
};
|
|
|
|
// Tastatureingaben verarbeiten
|
|
const handleKeyPress = (e: KeyboardEvent<HTMLDivElement>, index: string) => {
|
|
if (e.key === "Enter") {
|
|
handleSave(index);
|
|
} else if (e.key === "Escape") {
|
|
setEditingIndex("");
|
|
}
|
|
};
|
|
|
|
const handlePageKeyPress = (e: KeyboardEvent<HTMLDivElement>, index: string) => {
|
|
if (e.key === "Enter") {
|
|
handlePageSave(index);
|
|
} else if (e.key === "Escape") {
|
|
setEditingPageIndex("");
|
|
}
|
|
};
|
|
|
|
const handleNavigateToDetail = (settingName: string) => {
|
|
navigate({
|
|
to: "/extractedResult/$pitchBook/$kpi",
|
|
params: {
|
|
pitchBook: pdfId,
|
|
kpi: settingName,
|
|
},
|
|
search: { from: from ?? undefined },
|
|
});
|
|
};
|
|
|
|
return (
|
|
<TableContainer component={Paper}>
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell width="30%">
|
|
<strong>Kennzahl</strong>
|
|
</TableCell>
|
|
<TableCell width="55%">
|
|
<strong>Wert</strong>
|
|
</TableCell>
|
|
<TableCell align="center" width="15%">
|
|
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 1 }}>
|
|
<strong>Seite</strong>
|
|
</Box>
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
|
|
<TableBody>
|
|
{settings
|
|
.filter((setting) => setting.active)
|
|
.sort((a, b) => a.position - b.position)
|
|
.map((setting) => ({
|
|
setting: setting,
|
|
extractedValues: data[setting.name.toUpperCase()] || [],
|
|
}))
|
|
.map((row) => {
|
|
let borderColor = "transparent";
|
|
const hasMultipleValues = row.extractedValues.length > 1;
|
|
const hasNoValue =
|
|
row.setting.mandatory &&
|
|
(row.extractedValues.length === 0 ||
|
|
row.extractedValues.at(0)?.entity === "");
|
|
|
|
if (hasNoValue) {
|
|
borderColor = "red";
|
|
} else if (hasMultipleValues) {
|
|
borderColor = "#f6ed48";
|
|
}
|
|
|
|
const currentPage = row.extractedValues.at(0)?.page ?? 0;
|
|
const isPageHovered = hoveredPageIndex === row.setting.name;
|
|
const canEditPage = !hasMultipleValues;
|
|
|
|
return (
|
|
<TableRow key={row.setting.name}>
|
|
<TableCell>{row.setting.name}
|
|
{row.setting.mandatory && (
|
|
<span> *</span>
|
|
)}
|
|
</TableCell>
|
|
<TableCell
|
|
onClick={() => {
|
|
// Only allow inline editing for non-multiple value cells
|
|
if (!hasMultipleValues) {
|
|
startEditing(
|
|
row.extractedValues.at(0)?.entity || "",
|
|
row.setting.name,
|
|
);
|
|
} else {
|
|
// Navigate to detail page for multiple values
|
|
handleNavigateToDetail(row.setting.name);
|
|
}
|
|
}}
|
|
>
|
|
{hasMultipleValues ? (
|
|
<Tooltip
|
|
title={
|
|
<>
|
|
<b>Problem</b>
|
|
<br />
|
|
Mehrere Werte für die Kennzahl gefunden.
|
|
</>
|
|
}
|
|
placement="bottom"
|
|
arrow
|
|
>
|
|
<Box
|
|
sx={{
|
|
border: `2px solid ${borderColor}`,
|
|
borderRadius: 1,
|
|
padding: "4px 8px",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
width: "100%",
|
|
cursor: "pointer",
|
|
"&:hover": {
|
|
backgroundColor: "#f5f5f5",
|
|
},
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 1,
|
|
width: "100%",
|
|
}}
|
|
>
|
|
<span>
|
|
{row.extractedValues.at(0)?.entity || "—"}
|
|
</span>
|
|
</Box>
|
|
<SearchIcon
|
|
fontSize="small"
|
|
sx={{ color: "#f6ed48" }}
|
|
/>
|
|
</Box>
|
|
</Tooltip>
|
|
) : (
|
|
<Tooltip
|
|
title={
|
|
hasNoValue ? (
|
|
<>
|
|
<b>Problem</b>
|
|
<br />
|
|
Es wurden keine Kennzahlen gefunden. Bitte
|
|
ergänzen!
|
|
</>
|
|
) : (
|
|
""
|
|
)
|
|
}
|
|
placement="bottom"
|
|
arrow
|
|
>
|
|
<span>
|
|
<Box
|
|
sx={{
|
|
border: `2px solid ${borderColor}`,
|
|
borderRadius: 1,
|
|
padding: "4px 8px",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
width: "100%",
|
|
cursor: "text",
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 1,
|
|
width: "100%",
|
|
}}
|
|
>
|
|
{hasNoValue && (
|
|
<ErrorOutlineIcon
|
|
fontSize="small"
|
|
color="error"
|
|
/>
|
|
)}
|
|
{editingIndex === row.setting.name ? (
|
|
<TextField
|
|
value={editValue}
|
|
onChange={(e) => setEditValue(e.target.value)}
|
|
onKeyDown={(e) =>
|
|
handleKeyPress(e, row.setting.name)
|
|
}
|
|
onBlur={() => handleSave(row.setting.name)}
|
|
autoFocus
|
|
size="small"
|
|
fullWidth
|
|
variant="standard"
|
|
sx={{ margin: "-8px 0" }}
|
|
/>
|
|
) : (
|
|
<span>
|
|
{row.extractedValues.at(0)?.entity || "—"}
|
|
</span>
|
|
)}
|
|
</Box>
|
|
<EditIcon
|
|
fontSize="small"
|
|
sx={{ color: "#555", cursor: "pointer" }}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
startEditing(
|
|
row.extractedValues.at(0)?.entity || "",
|
|
row.setting.name,
|
|
);
|
|
}}
|
|
/>
|
|
</Box>
|
|
</span>
|
|
</Tooltip>
|
|
)}
|
|
</TableCell>
|
|
<TableCell align="center">
|
|
{editingPageIndex === row.setting.name ? (
|
|
<TextField
|
|
value={editPageValue}
|
|
onChange={(e) => {
|
|
const value = e.target.value;
|
|
if (value === '' || /^\d+$/.test(value)) {
|
|
setEditPageValue(value);
|
|
}
|
|
}}
|
|
onKeyDown={(e) => handlePageKeyPress(e, row.setting.name)}
|
|
onBlur={() => handlePageSave(row.setting.name)}
|
|
autoFocus
|
|
size="small"
|
|
variant="standard"
|
|
sx={{
|
|
width: "60px",
|
|
"& .MuiInput-input": {
|
|
textAlign: "center"
|
|
}
|
|
}}
|
|
inputProps={{
|
|
min: 0,
|
|
style: { textAlign: 'center' }
|
|
}}
|
|
/>
|
|
) : (
|
|
<>
|
|
{currentPage > 0 ? (
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
position: "relative",
|
|
cursor: "pointer",
|
|
borderRadius: "4px",
|
|
minHeight: "32px",
|
|
minWidth: "100px",
|
|
}}
|
|
onClick={() => {
|
|
if (canEditPage) {
|
|
startPageEditing(currentPage, row.setting.name);
|
|
}
|
|
}}
|
|
>
|
|
<Link
|
|
component="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
const extractedValue = row.extractedValues.at(0);
|
|
if (extractedValue?.page && extractedValue.page > 0) {
|
|
onPageClick?.(Number(extractedValue.page), extractedValue.entity || "");
|
|
}
|
|
}}
|
|
sx={{ cursor: "pointer" }}
|
|
>
|
|
{currentPage}
|
|
</Link>
|
|
</Box>
|
|
) : canEditPage ? (
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
position: "relative",
|
|
cursor: "pointer",
|
|
minHeight: "32px",
|
|
minWidth: "100px",
|
|
borderRadius: "4px",
|
|
backgroundColor: isPageHovered ? "#f8f9fa" : "transparent",
|
|
}}
|
|
onMouseEnter={() => setHoveredPageIndex(row.setting.name)}
|
|
onMouseLeave={() => setHoveredPageIndex("")}
|
|
onClick={() => startPageEditing(0, row.setting.name)}
|
|
>
|
|
<span style={{ color: "#999" }}>...</span>
|
|
<EditIcon
|
|
fontSize="small"
|
|
sx={{
|
|
position: "absolute",
|
|
left: "70px",
|
|
color: "black",
|
|
cursor: "pointer",
|
|
opacity: 0.7,
|
|
transition: "opacity 0.2s ease",
|
|
}}
|
|
/>
|
|
</Box>
|
|
) : (
|
|
""
|
|
)}
|
|
</>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
);
|
|
} |