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

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),
});