Merge pull request 'Data-Fetching Teil 1' (#59) from #53-data-fetching into main

Reviewed-on: #59
pull/61/head
Anastasia Hanna Ougolnikova 2025-06-09 15:25:43 +02:00
commit 211bb9a9d1
8 changed files with 442 additions and 219 deletions

View File

@ -1,6 +1,3 @@
from flask_socketio import SocketIO from flask_socketio import SocketIO
socketio = SocketIO( socketio = SocketIO(cors_allowed_origins="*")
cors_allowed_origins=["http://localhost:8080", "http://localhost:3000"],
transports=["polling", "websocket"],
)

View File

@ -1,211 +1,265 @@
import EditIcon from "@mui/icons-material/Edit";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import SearchIcon from "@mui/icons-material/Search";
import { import {
Table, TableBody, TableCell, TableContainer, Box,
TableHead, TableRow, Paper, Box, IconButton,
TextField, Link Link,
} from '@mui/material'; Paper,
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; Table,
import SearchIcon from '@mui/icons-material/Search'; TableBody,
import EditIcon from '@mui/icons-material/Edit'; TableCell,
import { useState, useEffect } from 'react'; TableContainer,
import type { KeyboardEvent } from 'react'; TableHead,
TableRow,
const API_BASE_URL = 'http://localhost:5050'; // Korrigierter Port für den Coordinator-Service TextField,
} from "@mui/material";
interface Kennzahl { import { useMutation, useQueryClient } from "@tanstack/react-query";
pdf_id: string; import { useNavigate } from "@tanstack/react-router";
label: string; import { useState } from "react";
value: string; import type { KeyboardEvent } from "react";
page: number; import { fetchPutKPI } from "../util/api";
status: 'ok' | 'error' | 'warning';
} const SETTINGS = [
{ name: "Rendite", position: 1, active: true, mandatory: true },
interface KennzahlenTableProps { { name: "Ausschüttungsrendite", position: 2, active: true, mandatory: true },
onPageClick?: (page: number) => void; { name: "Laufzeit", position: 3, active: true, mandatory: true },
pdfId?: string; // Neue Prop für die PDF-ID { name: "Länderallokation", position: 4, active: true, mandatory: true },
} { name: "Managmentgebühren", position: 5, active: true, mandatory: true },
{ name: "Risikoprofil", position: 6, active: false, mandatory: true },
// React-Komponente { name: "Irgendwas", position: 7, active: true, mandatory: true },
export default function KennzahlenTable({ onPageClick, pdfId = 'example' }: KennzahlenTableProps) { ];
const [rows, setRows] = useState<Kennzahl[]>([]);
const [editingIndex, setEditingIndex] = useState<number | null>(null); interface KennzahlenTableProps {
const [editValue, setEditValue] = useState(''); onPageClick?: (page: number) => void;
const [isLoading, setIsLoading] = useState(true); pdfId: string; // Neue Prop für die PDF-ID
const [error, setError] = useState<string | null>(null); data: {
[key: string]: {
// Initialisiere Beispieldaten label: string;
const initializeData = async () => { entity: string;
try { page: number;
const response = await fetch(`${API_BASE_URL}/api/kennzahlen/init`, { status: string;
method: 'POST' source: string;
}); }[];
if (!response.ok) { };
throw new Error('Fehler beim Initialisieren der Daten'); }
}
// Lade die Daten nach der Initialisierung // React-Komponente
await fetchKennzahlen(); export default function KennzahlenTable({
} catch (err) { onPageClick,
setError('Fehler beim Initialisieren der Daten'); data,
console.error('Fehler:', err); pdfId,
} }: KennzahlenTableProps) {
}; const [editingIndex, setEditingIndex] = useState<string>("");
const [editValue, setEditValue] = useState("");
// Lade Kennzahlen vom Backend const navigate = useNavigate({ from: "/extractedResult/$pitchBook" });
const fetchKennzahlen = async () => {
try { const queryClient = useQueryClient();
const response = await fetch(`${API_BASE_URL}/api/kennzahlen?pdf_id=${pdfId}`);
if (!response.ok) { const { mutate } = useMutation({
throw new Error('Fehler beim Laden der Kennzahlen'); mutationFn: (id: string) => {
} const key = id.toUpperCase();
const data = await response.json(); const updatedData = { ...data };
if (data.length === 0) { updatedData[key] = data[key]?.map((item) => ({
// Wenn keine Daten vorhanden sind, initialisiere Beispieldaten ...item,
await initializeData(); entity: editValue,
} else { })) || [{ label: key, entity: editValue }];
setRows(data); return fetchPutKPI(Number(pdfId), updatedData);
setError(null); },
} onMutate: async (id: string) => {
} catch (err) { await queryClient.cancelQueries({
setError('Fehler beim Laden der Daten'); queryKey: ["pitchBookKPI", pdfId],
console.error('Fehler:', err); });
} finally {
setIsLoading(false); const snapshot = queryClient.getQueryData(["pitchBookKPI", pdfId]);
}
}; const key = id.toUpperCase();
// Lade Daten beim ersten Render oder wenn sich die PDF-ID ändert queryClient.setQueryData(["pitchBookKPI", pdfId], () => {
useEffect(() => { const updatedData = { ...data };
fetchKennzahlen(); updatedData[key] = data[key]?.map((item) => ({
}, [pdfId]); ...item,
entity: editValue,
// Funktion zum Senden der PUT-Anfrage })) || [{ label: key, entity: editValue }];
const updateKennzahl = async (label: string, value: string) => { return updatedData;
try { });
const response = await fetch(`${API_BASE_URL}/api/kennzahlen/${label}?pdf_id=${pdfId}`, {
method: 'PUT', return () => {
headers: { queryClient.setQueryData(["pitchBookKPI", pdfId], snapshot);
'Content-Type': 'application/json', };
}, },
body: JSON.stringify({ value }) onError: (error, _variables, rollback) => {
}); console.log("error", error);
rollback?.();
if (!response.ok) { },
throw new Error('Fehler beim Speichern der Kennzahl'); onSettled: () => {
} return queryClient.invalidateQueries({
queryKey: ["pitchBookKPI", pdfId],
// Lade die Daten neu nach dem Update });
await fetchKennzahlen(); },
} catch (error) { });
console.error('Fehler:', error);
setError('Fehler beim Speichern der Änderungen'); // Bearbeitung starten
} const startEditing = (value: string, index: string) => {
}; setEditingIndex(index);
setEditValue(value);
// Bearbeitung starten };
const startEditing = (value: string, index: number) => {
setEditingIndex(index); // Bearbeitung beenden und Wert speichern
setEditValue(value); const handleSave = async (index: string) => {
}; // await updateKennzahl(rows[index].label, editValue);
mutate(index);
// Bearbeitung beenden und Wert speichern setEditingIndex("");
const handleSave = async (index: number) => { };
await updateKennzahl(rows[index].label, editValue);
setEditingIndex(null); // Tastatureingaben verarbeiten
}; const handleKeyPress = (e: KeyboardEvent<HTMLDivElement>, index: string) => {
if (e.key === "Enter") {
// Tastatureingaben verarbeiten handleSave(index);
const handleKeyPress = (e: KeyboardEvent<HTMLDivElement>, index: number) => { } else if (e.key === "Escape") {
if (e.key === 'Enter') { setEditingIndex("null");
handleSave(index); }
} else if (e.key === 'Escape') { };
setEditingIndex(null);
} return (
}; <TableContainer component={Paper}>
<Table>
if (isLoading) { <TableHead>
return <div>Lade Daten...</div>; <TableRow>
} <TableCell>
<strong>Kennzahl</strong>
if (error) { </TableCell>
return <Box sx={{ color: 'error.main' }}>{error}</Box>; <TableCell>
} <strong>Wert</strong>
</TableCell>
return ( <TableCell>
<TableContainer component={Paper}> <strong>Seite</strong>
<Table> </TableCell>
<TableHead> </TableRow>
<TableRow> </TableHead>
<TableCell><strong>Kennzahl</strong></TableCell>
<TableCell><strong>Wert</strong></TableCell> <TableBody>
<TableCell><strong>Seite</strong></TableCell> {SETTINGS.filter((setting) => setting.active)
</TableRow> .sort((a, b) => a.position - b.position)
</TableHead> .map((setting) => ({
setting: setting,
<TableBody> extractedValues: data[setting.name.toUpperCase()] || [],
{rows.map((row, index) => { }))
let borderColor = 'transparent'; .map((row) => {
if (row.status === 'error') borderColor = 'red'; let borderColor = "transparent";
else if (row.status === 'warning') borderColor = '#f6ed48'; if (
row.setting.mandatory &&
return ( (row.extractedValues.length === 0 ||
<TableRow key={index}> row.extractedValues.at(0)?.entity === "")
<TableCell>{row.label}</TableCell> )
<TableCell onClick={() => startEditing(row.value, index)}> borderColor = "red";
<Box else if (row.extractedValues.length > 1) borderColor = "#f6ed48";
sx={{
border: `2px solid ${borderColor}`, return (
borderRadius: 1, <TableRow key={row.setting.name}>
padding: '4px 8px', <TableCell>{row.setting.name}</TableCell>
display: 'flex', <TableCell
alignItems: 'center', onClick={() =>
justifyContent: 'space-between', startEditing(
width: '100%', row.extractedValues.at(0)?.entity || "",
cursor: 'text', row.setting.name,
}} )
> }
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%' }}> >
{row.status === 'error' && <ErrorOutlineIcon fontSize="small" color="error" />} <Box
{row.status === 'warning' && <SearchIcon fontSize="small" sx={{ color: '#f6ed48' }} />} sx={{
{editingIndex === index ? ( border: `2px solid ${borderColor}`,
<TextField borderRadius: 1,
value={editValue} padding: "4px 8px",
onChange={(e) => setEditValue(e.target.value)} display: "flex",
onKeyDown={(e) => handleKeyPress(e, index)} alignItems: "center",
onBlur={() => handleSave(index)} justifyContent: "space-between",
autoFocus width: "100%",
size="small" cursor: "text",
fullWidth }}
variant="standard" >
sx={{ margin: '-8px 0' }} <Box
/> sx={{
) : ( display: "flex",
<span>{row.value || '—'}</span> alignItems: "center",
)} gap: 1,
</Box> width: "100%",
<EditIcon }}
fontSize="small" >
sx={{ color: '#555', cursor: 'pointer' }} {row.setting.mandatory &&
onClick={(e) => { row.extractedValues.length === 0 && (
e.stopPropagation(); <ErrorOutlineIcon fontSize="small" color="error" />
startEditing(row.value, index); )}
}} {editingIndex === row.setting.name ? (
/> <TextField
</Box> value={editValue}
</TableCell> onChange={(e) => setEditValue(e.target.value)}
<TableCell> onKeyDown={(e) =>
<Link handleKeyPress(e, row.setting.name)
component="button" }
onClick={() => onPageClick?.(row.page)} onBlur={() => handleSave(row.setting.name)}
sx={{ cursor: 'pointer' }} autoFocus
> size="small"
{row.page} fullWidth
</Link> variant="standard"
</TableCell> sx={{ margin: "-8px 0" }}
</TableRow> />
); ) : (
})} <span>
</TableBody> {row.extractedValues.at(0)?.entity || "—"}
</Table> </span>
</TableContainer> )}
); </Box>
} {row.extractedValues.length > 1 && (
<IconButton
aria-label="select"
onClick={() =>
navigate({
to: "/extractedResult/$pitchBook/$kpi",
params: {
pitchBook: pdfId,
kpi: row.setting.name,
},
})
}
>
<SearchIcon
fontSize="small"
sx={{ color: "#f6ed48" }}
/>
</IconButton>
)}
{row.extractedValues.length <= 1 && (
<EditIcon
fontSize="small"
sx={{ color: "#555", cursor: "pointer" }}
onClick={(e) => {
e.stopPropagation();
startEditing(
row.extractedValues.at(0)?.entity || "",
row.setting.name,
);
}}
/>
)}
</Box>
</TableCell>
<TableCell>
<Link
component="button"
onClick={() =>
onPageClick?.(Number(row.extractedValues.at(0)?.page))
}
sx={{ cursor: "pointer" }}
>
{row.extractedValues.at(0)?.page}
</Link>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
);
}

View File

@ -12,7 +12,10 @@ interface PDFViewerProps {
currentPage?: number; currentPage?: number;
} }
export default function PDFViewer({ pitchBookId, currentPage }: PDFViewerProps) { export default function PDFViewer({
pitchBookId,
currentPage,
}: PDFViewerProps) {
const [numPages, setNumPages] = useState<number | null>(null); const [numPages, setNumPages] = useState<number | null>(null);
const [pageNumber, setPageNumber] = useState(currentPage || 1); const [pageNumber, setPageNumber] = useState(currentPage || 1);
const [containerWidth, setContainerWidth] = useState<number | null>(null); const [containerWidth, setContainerWidth] = useState<number | null>(null);
@ -39,7 +42,7 @@ export default function PDFViewer({ pitchBookId, currentPage }: PDFViewerProps)
if (currentPage && currentPage !== pageNumber) { if (currentPage && currentPage !== pageNumber) {
setPageNumber(currentPage); setPageNumber(currentPage);
} }
}, [currentPage]); }, [currentPage, pageNumber]);
useEffect(() => { useEffect(() => {
const handleProgress = (data: { id: number; progress: number }) => { const handleProgress = (data: { id: number; progress: number }) => {
@ -115,4 +118,4 @@ export default function PDFViewer({ pitchBookId, currentPage }: PDFViewerProps)
</Box> </Box>
</Box> </Box>
); );
} }

View File

@ -16,6 +16,7 @@ import { Route as ConfigImport } from './routes/config'
import { Route as IndexImport } from './routes/index' import { Route as IndexImport } from './routes/index'
import { Route as ExtractedResultPitchBookImport } from './routes/extractedResult.$pitchBook' import { Route as ExtractedResultPitchBookImport } from './routes/extractedResult.$pitchBook'
import { Route as ConfigDetailKpiIdImport } from './routes/config-detail.$kpiId' import { Route as ConfigDetailKpiIdImport } from './routes/config-detail.$kpiId'
import { Route as ExtractedResultPitchBookKpiImport } from './routes/extractedResult_.$pitchBook.$kpi'
// Create/Update Routes // Create/Update Routes
@ -49,6 +50,13 @@ const ConfigDetailKpiIdRoute = ConfigDetailKpiIdImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const ExtractedResultPitchBookKpiRoute =
ExtractedResultPitchBookKpiImport.update({
id: '/extractedResult_/$pitchBook/$kpi',
path: '/extractedResult/$pitchBook/$kpi',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface // Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
@ -88,6 +96,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ExtractedResultPitchBookImport preLoaderRoute: typeof ExtractedResultPitchBookImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/extractedResult_/$pitchBook/$kpi': {
id: '/extractedResult_/$pitchBook/$kpi'
path: '/extractedResult/$pitchBook/$kpi'
fullPath: '/extractedResult/$pitchBook/$kpi'
preLoaderRoute: typeof ExtractedResultPitchBookKpiImport
parentRoute: typeof rootRoute
}
} }
} }
@ -99,6 +114,7 @@ export interface FileRoutesByFullPath {
'/config-add': typeof ConfigAddRoute '/config-add': typeof ConfigAddRoute
'/config-detail/$kpiId': typeof ConfigDetailKpiIdRoute '/config-detail/$kpiId': typeof ConfigDetailKpiIdRoute
'/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute '/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute
'/extractedResult/$pitchBook/$kpi': typeof ExtractedResultPitchBookKpiRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
@ -107,6 +123,7 @@ export interface FileRoutesByTo {
'/config-add': typeof ConfigAddRoute '/config-add': typeof ConfigAddRoute
'/config-detail/$kpiId': typeof ConfigDetailKpiIdRoute '/config-detail/$kpiId': typeof ConfigDetailKpiIdRoute
'/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute '/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute
'/extractedResult/$pitchBook/$kpi': typeof ExtractedResultPitchBookKpiRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
@ -116,6 +133,7 @@ export interface FileRoutesById {
'/config-add': typeof ConfigAddRoute '/config-add': typeof ConfigAddRoute
'/config-detail/$kpiId': typeof ConfigDetailKpiIdRoute '/config-detail/$kpiId': typeof ConfigDetailKpiIdRoute
'/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute '/extractedResult/$pitchBook': typeof ExtractedResultPitchBookRoute
'/extractedResult_/$pitchBook/$kpi': typeof ExtractedResultPitchBookKpiRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
@ -126,6 +144,7 @@ export interface FileRouteTypes {
| '/config-add' | '/config-add'
| '/config-detail/$kpiId' | '/config-detail/$kpiId'
| '/extractedResult/$pitchBook' | '/extractedResult/$pitchBook'
| '/extractedResult/$pitchBook/$kpi'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
@ -133,6 +152,7 @@ export interface FileRouteTypes {
| '/config-add' | '/config-add'
| '/config-detail/$kpiId' | '/config-detail/$kpiId'
| '/extractedResult/$pitchBook' | '/extractedResult/$pitchBook'
| '/extractedResult/$pitchBook/$kpi'
id: id:
| '__root__' | '__root__'
| '/' | '/'
@ -140,6 +160,7 @@ export interface FileRouteTypes {
| '/config-add' | '/config-add'
| '/config-detail/$kpiId' | '/config-detail/$kpiId'
| '/extractedResult/$pitchBook' | '/extractedResult/$pitchBook'
| '/extractedResult_/$pitchBook/$kpi'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
@ -149,6 +170,7 @@ export interface RootRouteChildren {
ConfigAddRoute: typeof ConfigAddRoute ConfigAddRoute: typeof ConfigAddRoute
ConfigDetailKpiIdRoute: typeof ConfigDetailKpiIdRoute ConfigDetailKpiIdRoute: typeof ConfigDetailKpiIdRoute
ExtractedResultPitchBookRoute: typeof ExtractedResultPitchBookRoute ExtractedResultPitchBookRoute: typeof ExtractedResultPitchBookRoute
ExtractedResultPitchBookKpiRoute: typeof ExtractedResultPitchBookKpiRoute
} }
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
@ -157,6 +179,7 @@ const rootRouteChildren: RootRouteChildren = {
ConfigAddRoute: ConfigAddRoute, ConfigAddRoute: ConfigAddRoute,
ConfigDetailKpiIdRoute: ConfigDetailKpiIdRoute, ConfigDetailKpiIdRoute: ConfigDetailKpiIdRoute,
ExtractedResultPitchBookRoute: ExtractedResultPitchBookRoute, ExtractedResultPitchBookRoute: ExtractedResultPitchBookRoute,
ExtractedResultPitchBookKpiRoute: ExtractedResultPitchBookKpiRoute,
} }
export const routeTree = rootRoute export const routeTree = rootRoute
@ -173,7 +196,8 @@ export const routeTree = rootRoute
"/config", "/config",
"/config-add", "/config-add",
"/config-detail/$kpiId", "/config-detail/$kpiId",
"/extractedResult/$pitchBook" "/extractedResult/$pitchBook",
"/extractedResult_/$pitchBook/$kpi"
] ]
}, },
"/": { "/": {
@ -190,6 +214,9 @@ export const routeTree = rootRoute
}, },
"/extractedResult/$pitchBook": { "/extractedResult/$pitchBook": {
"filePath": "extractedResult.$pitchBook.tsx" "filePath": "extractedResult.$pitchBook.tsx"
},
"/extractedResult_/$pitchBook/$kpi": {
"filePath": "extractedResult_.$pitchBook.$kpi.tsx"
} }
} }
} }

View File

@ -1,12 +1,16 @@
import ContentPasteIcon from "@mui/icons-material/ContentPaste"; import ContentPasteIcon from "@mui/icons-material/ContentPaste";
import { Box, Button, Paper, Typography } from "@mui/material"; import { Box, Button, Paper, Typography } from "@mui/material";
import { useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
import KennzahlenTable from "../components/KennzahlenTable"; import KennzahlenTable from "../components/KennzahlenTable";
import PDFViewer from "../components/pdfViewer"; import PDFViewer from "../components/pdfViewer";
import { kpiQueryOptions } from "../util/query";
export const Route = createFileRoute("/extractedResult/$pitchBook")({ export const Route = createFileRoute("/extractedResult/$pitchBook")({
component: ExtractedResultsPage, component: ExtractedResultsPage,
loader: ({ context: { queryClient }, params: { pitchBook } }) =>
queryClient.ensureQueryData(kpiQueryOptions(pitchBook)),
}); });
function ExtractedResultsPage() { function ExtractedResultsPage() {
@ -21,6 +25,8 @@ function ExtractedResultsPage() {
green: "#3fd942", green: "#3fd942",
}[status]; }[status];
const { data: kpi } = useSuspenseQuery(kpiQueryOptions(pitchBook));
return ( return (
<Box p={4}> <Box p={4}>
<Box display="flex" alignItems="center" gap={3}> <Box display="flex" alignItems="center" gap={3}>
@ -60,7 +66,11 @@ function ExtractedResultsPage() {
overflow: "auto", overflow: "auto",
}} }}
> >
<KennzahlenTable onPageClick={setCurrentPage} /> <KennzahlenTable
onPageClick={setCurrentPage}
data={kpi}
pdfId={pitchBook}
/>
</Paper> </Paper>
<Box <Box
display="flex" display="flex"

View File

@ -0,0 +1,27 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { kpiQueryOptions } from "../util/query";
export const Route = createFileRoute("/extractedResult_/$pitchBook/$kpi")({
component: RouteComponent,
loader: ({ context: { queryClient }, params: { pitchBook } }) =>
queryClient.ensureQueryData(kpiQueryOptions(pitchBook)),
});
function RouteComponent() {
const { pitchBook, kpi } = Route.useParams();
const {
data: { [kpi.toUpperCase()]: kpiValues },
} = useSuspenseQuery(kpiQueryOptions(pitchBook));
return (
<div>
{kpiValues.map((e) => (
<div key={`${e.entity}_${e.page}`}>
{e.label}: {e.entity}
</div>
))}
</div>
);
}

View File

@ -0,0 +1,97 @@
export const fetchKPI = async (
pitchBookId: string,
): Promise<{
[key: string]: {
label: string;
entity: string;
page: number;
status: string;
source: string;
}[];
}> => {
const response = await fetch(
`http://localhost:5050/api/pitch_book/${pitchBookId}`,
);
const data = await response.json();
return getKPI(data.kpi);
};
export const fetchPutKPI = async (
pitchBookId: number,
kpi: {
[key: string]: {
label: string;
entity: string;
page: number;
status: string;
source: string;
}[];
},
): Promise<{
[key: string]: {
label: string;
entity: string;
page: number;
status: string;
source: string;
}[];
}> => {
const formData = new FormData();
formData.append("kpi", JSON.stringify(flattenKPIArray(kpi)));
const response = await fetch(
`http://localhost:5050/api/pitch_book/${pitchBookId}`,
{
method: "PUT",
body: formData,
},
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return getKPI(data.kpi);
};
const getKPI = (data: string) => {
const kpi = JSON.parse(data) as {
label: string;
entity: string;
page: number;
status: string;
source: string;
}[];
const reducedKpi = kpi.reduce(
(prev, curr) => {
if (!prev[curr.label]) {
prev[curr.label] = [];
}
prev[curr.label].push(curr);
return prev;
},
{} as {
[key: string]: {
label: string;
entity: string;
page: number;
status: string;
source: string;
}[];
},
);
return reducedKpi;
};
export const flattenKPIArray = (kpi: {
[key: string]: {
label: string;
entity: string;
page: number;
status: string;
source: string;
}[];
}) => {
return Object.values(kpi).flat();
};

View File

@ -0,0 +1,8 @@
import { queryOptions } from "@tanstack/react-query";
import { fetchKPI } from "./api";
export const kpiQueryOptions = (pitchBookId: string) =>
queryOptions({
queryKey: ["pitchBookKPI", pitchBookId],
queryFn: () => fetchKPI(pitchBookId),
});