pse2_ff/project/frontend/src/components/PitchBooksTable.tsx

403 lines
10 KiB
TypeScript

import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import HourglassEmptyIcon from "@mui/icons-material/HourglassEmpty";
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
import {
Box,
Chip,
CircularProgress,
LinearProgress,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from "@mui/material";
import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import { useCallback, useEffect, useState } from "react";
import { socket } from "../socket";
import { fetchPitchBooksById } from "../util/api";
import { pitchBooksQueryOptions } from "../util/query";
import { formatDate } from "../util/date"
interface PitchBook {
id: number;
filename: string;
created_at: string;
kpi?:
| string
| {
[key: string]: {
label: string;
entity: string;
page: number;
status: string;
source: string;
}[];
};
status?: "processing" | "completed";
}
export function PitchBooksTable() {
const [loadingPitchBooks, setLoadingPitchBooks] = useState<
{
id: number;
progress: number;
filename?: string;
created_at?: string;
buffer: number;
intervalId?: number;
}[]
>([]);
const navigate = useNavigate();
const { data: pitchBooks, isLoading } = useSuspenseQuery(
pitchBooksQueryOptions(),
);
const handleRowClick = (pitchBookId: number) => {
navigate({
to: "/extractedResult/$pitchBook",
params: { pitchBook: pitchBookId.toString() },
search: { from: "overview" },
});
};
const onConnection = useCallback(() => {
console.log("connected");
}, []);
const queryClient = useQueryClient();
const onProgress = useCallback(
(progress: { id: number; progress: number }) => {
if (progress.progress === 100) {
setLoadingPitchBooks((prev) => {
const intervalId = prev.find(
(item) => item.id === progress.id,
)?.intervalId;
intervalId && clearInterval(intervalId);
return [...prev.filter((item) => item.id !== progress.id)];
});
queryClient.invalidateQueries({
queryKey: pitchBooksQueryOptions().queryKey,
});
} else {
setLoadingPitchBooks((prev) => {
const oldItem = prev.find((item) => item.id === progress.id);
let intervalId = oldItem?.intervalId;
if (!oldItem) {
intervalId = setInterval(() => {
setLoadingPitchBooks((prev) => {
const oldItem = prev.find((item) => item.id === progress.id);
if (!oldItem) return prev;
return [
...prev.filter((e) => e.id !== progress.id),
{
id: progress.id,
progress: oldItem?.progress ?? progress.progress,
filename: oldItem?.filename,
buffer: oldItem ? oldItem.buffer + 0.5 : 0,
intervalId: oldItem.intervalId,
created_at: oldItem?.created_at,
},
];
});
}, 400);
fetchPitchBooksById(progress.id)
.then((res) => {
setLoadingPitchBooks((prev) => [
...prev.filter((item) => item.id !== progress.id),
{
id: progress.id,
progress: progress.progress,
filename: res.filename,
buffer: 0,
intervalId,
created_at: res.created_at,
},
]);
})
.catch((err) => {
console.error(err);
});
}
return [
...prev.filter((item) => item.id !== progress.id),
{
id: progress.id,
progress: progress.progress,
filename: oldItem?.filename,
created_at: oldItem?.created_at,
buffer: 0,
intervalId,
},
];
});
}
},
[queryClient],
);
useEffect(() => {
socket.on("connect", onConnection);
socket.on("progress", onProgress);
return () => {
socket.off("connect", onConnection);
socket.off("progress", onProgress);
};
}, [onConnection, onProgress]);
const getKPIValue = (pitchBook: PitchBook, fieldName: string): string => {
if (!pitchBook.kpi || typeof pitchBook.kpi === "string") {
try {
const parsedKPI = JSON.parse(pitchBook.kpi as string);
// Convert array to object format if needed
const kpiObj = Array.isArray(parsedKPI)
? parsedKPI.reduce((acc, item) => {
if (!acc[item.label]) acc[item.label] = [];
acc[item.label].push(item);
return acc;
}, {})
: parsedKPI;
return kpiObj[fieldName]?.[0]?.entity || "N/A";
} catch {
return "N/A";
}
}
return pitchBook.kpi[fieldName]?.[0]?.entity || "N/A";
};
const getStatus = (pitchBook: PitchBook) => {
if (
pitchBook.kpi &&
((typeof pitchBook.kpi === "string" && pitchBook.kpi !== "{}") ||
(typeof pitchBook.kpi === "object" &&
Object.keys(pitchBook.kpi).length > 0))
) {
return "completed";
}
return "processing";
};
if (isLoading) {
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="400px"
>
<CircularProgress sx={{ color: "#383838" }} />
</Box>
);
}
return (
<TableContainer
component={Paper}
sx={{
width: "85%",
maxWidth: 1200,
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
}}
>
<Table>
<TableHead>
<TableRow sx={{ backgroundColor: "#f5f5f5" }}>
<TableCell sx={{ width: "60px" }} />
<TableCell sx={{ fontWeight: "bold" }}>Fondsname</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>Fondsmanager</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>Hochgeladen am</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>Dateiname</TableCell>
<TableCell sx={{ fontWeight: "bold", width: "120px" }}>
Status
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loadingPitchBooks
.sort((a, b) => a.id - b.id)
.map((pitchBook) => (
<TableRow key={pitchBook.id}>
<TableCell>
<Box
sx={{
width: 40,
height: 50,
backgroundColor: "#f0f0f0",
borderRadius: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
border: "1px solid #e0e0e0",
}}
>
<PictureAsPdfIcon fontSize="small" sx={{ color: "#666" }} />
</Box>
</TableCell>
<TableCell colSpan={2}>
<LinearProgress
variant="buffer"
value={pitchBook.progress}
valueBuffer={
pitchBook.buffer
? pitchBook.progress + pitchBook.buffer
: pitchBook.progress
}
/>
</TableCell>
<TableCell>
<Typography
variant="body2"
color="text.secondary"
fontSize="0.875rem"
>
{pitchBook.created_at && formatDate(pitchBook.created_at)}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body2"
color="text.secondary"
fontSize="0.875rem"
>
{pitchBook.filename}
</Typography>
</TableCell>
<TableCell>
<Chip
icon={<HourglassEmptyIcon />}
label="In Bearbeitung"
size="small"
sx={{
backgroundColor: "#fff3e0",
color: "#e65100",
"& .MuiChip-icon": {
color: "#e65100",
},
}}
/>
</TableCell>
</TableRow>
))}
{pitchBooks
.filter(
(pitchbook: PitchBook) =>
!loadingPitchBooks.some((e) => e.id === pitchbook.id),
)
.sort(
(a: PitchBook, b: PitchBook) =>
new Date(b.created_at).getTime() -
new Date(a.created_at).getTime(),
)
.map((pitchBook: PitchBook) => {
const status = getStatus(pitchBook);
const fundName =
getKPIValue(pitchBook, "FONDSNAME") ||
getKPIValue(pitchBook, "FUND_NAME") ||
getKPIValue(pitchBook, "NAME");
const manager =
getKPIValue(pitchBook, "FONDSMANAGER") ||
getKPIValue(pitchBook, "MANAGER") ||
getKPIValue(pitchBook, "PORTFOLIO_MANAGER");
return (
<TableRow
key={pitchBook.id}
onClick={() => handleRowClick(pitchBook.id)}
sx={{
cursor: "pointer",
"&:hover": {
backgroundColor: "#f9f9f9",
},
}}
>
<TableCell>
<Box
sx={{
width: 40,
height: 50,
backgroundColor: "#f0f0f0",
borderRadius: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
border: "1px solid #e0e0e0",
}}
>
<PictureAsPdfIcon
fontSize="small"
sx={{ color: "#666" }}
/>
</Box>
</TableCell>
<TableCell>
<Typography variant="body2" fontWeight="medium">
{fundName}
</Typography>
</TableCell>
<TableCell>{manager}</TableCell>
<TableCell>{formatDate(pitchBook.created_at)}</TableCell>
<TableCell>
<Typography
variant="body2"
color="text.secondary"
fontSize="0.875rem"
>
{pitchBook.filename}
</Typography>
</TableCell>
<TableCell>
{status === "completed" ? (
<Chip
icon={<CheckCircleIcon />}
label="Extraktion Abgeschlossen"
size="small"
sx={{
backgroundColor: "#e8f5e9",
color: "#2e7d32",
"& .MuiChip-icon": {
color: "#2e7d32",
},
}}
/>
) : (
<Chip
icon={<HourglassEmptyIcon />}
label="In Bearbeitung"
size="small"
sx={{
backgroundColor: "#fff3e0",
color: "#e65100",
"& .MuiChip-icon": {
color: "#e65100",
},
}}
/>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
{pitchBooks.length === 0 && (
<Box p={4} textAlign="center">
<Typography color="text.secondary">
Keine Pitch Books vorhanden
</Typography>
</Box>
)}
</TableContainer>
);
}