Merge branch 'main' into #54-async-ocr

# Conflicts:
#	project/backend/coordinator/app.py
pull/57/head
Jaronim Pracht 2025-06-07 12:48:22 +02:00
commit 081aa0e936
5 changed files with 277 additions and 120 deletions

View File

@ -5,6 +5,7 @@ from dotenv import load_dotenv
from controller import register_routes
from model.database import init_db
from controller.socketIO import socketio
from controller.kennzahlen import kennzahlen_bp
app = Flask(__name__)
CORS(app)
@ -21,6 +22,9 @@ init_db(app)
register_routes(app)
# Register blueprints
app.register_blueprint(kennzahlen_bp)
@app.route("/health")
def health_check():
return "OK"

View File

@ -0,0 +1,61 @@
from flask import Blueprint, jsonify, request
from model.kennzahl import Kennzahl
from model.database import db
kennzahlen_bp = Blueprint('kennzahlen', __name__)
# Beispieldaten
EXAMPLE_DATA = [
{"pdf_id": "example", "label": "Fondsname", "value": "Fund Real Estate Prime Europe", "page": 1, "status": "ok"},
{"pdf_id": "example", "label": "Fondsmanager", "value": "", "page": 1, "status": "error"},
{"pdf_id": "example", "label": "Risikoprofil", "value": "Core/Core+", "page": 10, "status": "warning"},
{"pdf_id": "example", "label": "LTV", "value": "30-35 %", "page": 8, "status": "ok"},
{"pdf_id": "example", "label": "Ausschüttungsrendite", "value": "4%", "page": 34, "status": "ok"}
]
@kennzahlen_bp.route('/api/kennzahlen/init', methods=['POST'])
def init_kennzahlen():
try:
# Lösche existierende Beispieldaten
Kennzahl.query.filter_by(pdf_id='example').delete()
# Füge Beispieldaten ein
for data in EXAMPLE_DATA:
kennzahl = Kennzahl(
pdf_id=data['pdf_id'],
label=data['label'],
value=data['value'],
page=data['page'],
status=data['status']
)
db.session.add(kennzahl)
db.session.commit()
return jsonify({"message": "Kennzahlen erfolgreich initialisiert"})
except Exception as e:
db.session.rollback()
return jsonify({"error": str(e)}), 500
@kennzahlen_bp.route('/api/kennzahlen', methods=['GET'])
def get_kennzahlen():
pdf_id = request.args.get('pdf_id', 'example') # Default zu 'example' für Beispieldaten
kennzahlen = Kennzahl.query.filter_by(pdf_id=pdf_id).all()
return jsonify([k.to_dict() for k in kennzahlen])
@kennzahlen_bp.route('/api/kennzahlen/<label>', methods=['PUT'])
def update_kennzahl(label):
data = request.get_json()
pdf_id = request.args.get('pdf_id', 'example') # Default zu 'example' für Beispieldaten
kennzahl = Kennzahl.query.filter_by(pdf_id=pdf_id, label=label).first()
if not kennzahl:
return jsonify({'error': 'Kennzahl nicht gefunden'}), 404
kennzahl.value = data.get('value', kennzahl.value)
db.session.commit()
return jsonify(kennzahl.to_dict())

View File

@ -0,0 +1,26 @@
from .database import db
class Kennzahl(db.Model):
__tablename__ = 'kennzahlen'
id = db.Column(db.Integer, primary_key=True)
pdf_id = db.Column(db.String(100), nullable=False) # ID des PDFs
label = db.Column(db.String(100), nullable=False)
value = db.Column(db.String(100))
page = db.Column(db.Integer)
status = db.Column(db.String(20))
# Zusammengesetzter Unique-Constraint für pdf_id und label
__table_args__ = (
db.UniqueConstraint('pdf_id', 'label', name='unique_pdf_kennzahl'),
)
def to_dict(self):
return {
'pdf_id': self.pdf_id,
'label': self.label,
'value': self.value,
'page': self.page,
'status': self.status
}

View File

@ -8,8 +8,8 @@ services:
image: postgres:17-alpine
env_file:
- .env
ports:
- "5432:5432"
# ports:
# - "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin"]
interval: 10s

View File

@ -1,145 +1,211 @@
import {
Table, TableBody, TableCell, TableContainer,
TableHead, TableRow, Paper, Box,
Dialog, DialogActions, DialogContent, DialogTitle,
TextField, Button, Link
TextField, Link
} from '@mui/material';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import SearchIcon from '@mui/icons-material/Search';
import EditIcon from '@mui/icons-material/Edit';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import type { KeyboardEvent } from 'react';
const API_BASE_URL = 'http://localhost:5050'; // Korrigierter Port für den Coordinator-Service
// Beispiel-Daten
const exampleData = [
{ label: 'Fondsname', value: 'Fund Real Estate Prime Europe', page: 1, status: 'ok' },
{ label: 'Fondsmanager', value: '', page: 1, status: 'error' },
{ label: 'Risikoprofil', value: 'Core/Core+', page: 10, status: 'warning' },
{ label: 'LTV', value: '30-35 %', page: 8, status: 'ok' },
{ label: 'Ausschüttungsrendite', value: '4%', page: 34, status: 'ok' }
];
interface Kennzahl {
pdf_id: string;
label: string;
value: string;
page: number;
status: 'ok' | 'error' | 'warning';
}
interface KennzahlenTableProps {
onPageClick?: (page: number) => void;
pdfId?: string; // Neue Prop für die PDF-ID
}
// React-Komponente
export default function KennzahlenTable({ onPageClick }: KennzahlenTableProps) {
// Zustand für bearbeitbare Daten
const [rows, setRows] = useState(exampleData);
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);
// Zustände für Dialog-Funktion
const [open, setOpen] = useState(false); // Dialog anzeigen?
const [currentValue, setCurrentValue] = useState(''); // Eingabewert
const [currentIndex, setCurrentIndex] = useState<number | null>(null); // Zeilenindex
// Beim Klick auf das Stift-Icon: Dialog öffnen
const handleEditClick = (value: string, index: number) => {
setCurrentValue(value);
setCurrentIndex(index);
setOpen(true);
};
// Wert speichern und Dialog schließen
const handleSave = () => {
if (currentIndex !== null) {
const updated = [...rows];
updated[currentIndex].value = currentValue;
setRows(updated);
// Initialisiere Beispieldaten
const initializeData = async () => {
try {
const response = await fetch(`${API_BASE_URL}/api/kennzahlen/init`, {
method: 'POST'
});
if (!response.ok) {
throw new Error('Fehler beim Initialisieren der Daten');
}
// Lade die Daten nach der Initialisierung
await fetchKennzahlen();
} catch (err) {
setError('Fehler beim Initialisieren der Daten');
console.error('Fehler:', err);
}
setOpen(false);
};
// Lade Kennzahlen vom Backend
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
useEffect(() => {
fetchKennzahlen();
}, [pdfId]);
// Funktion zum Senden der PUT-Anfrage
const updateKennzahl = async (label: string, value: string) => {
try {
const response = await fetch(`${API_BASE_URL}/api/kennzahlen/${label}?pdf_id=${pdfId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ value })
});
if (!response.ok) {
throw new Error('Fehler beim Speichern der Kennzahl');
}
// 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: number) => {
setEditingIndex(index);
setEditValue(value);
};
// Bearbeitung beenden und Wert speichern
const handleSave = async (index: number) => {
await updateKennzahl(rows[index].label, editValue);
setEditingIndex(null);
};
// Tastatureingaben verarbeiten
const handleKeyPress = (e: KeyboardEvent<HTMLDivElement>, index: number) => {
if (e.key === 'Enter') {
handleSave(index);
} else if (e.key === 'Escape') {
setEditingIndex(null);
}
};
if (isLoading) {
return <div>Lade Daten...</div>;
}
if (error) {
return <Box sx={{ color: 'error.main' }}>{error}</Box>;
}
return (
<>
<TableContainer component={Paper}>
<Table>
{/* Tabellenkopf */}
<TableHead>
<TableRow>
<TableCell><strong>Kennzahl</strong></TableCell>
<TableCell><strong>Wert</strong></TableCell>
<TableCell><strong>Seite</strong></TableCell>
</TableRow>
</TableHead>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell><strong>Kennzahl</strong></TableCell>
<TableCell><strong>Wert</strong></TableCell>
<TableCell><strong>Seite</strong></TableCell>
</TableRow>
</TableHead>
{/* Tabelleninhalt */}
<TableBody>
{rows.map((row, index) => {
// Rahmenfarbe anhand Status
let borderColor = 'transparent';
if (row.status === 'error') borderColor = 'red';
else if (row.status === 'warning') borderColor = '#f6ed48';
<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}>
{/* Kennzahl */}
<TableCell>{row.label}</TableCell>
{/* Wert mit Status-Icons + Stift rechts */}
<TableCell>
<Box
sx={{
border: `2px solid ${borderColor}`,
borderRadius: 1,
padding: '4px 8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{row.status === 'error' && <ErrorOutlineIcon fontSize="small" color="error" />}
{row.status === 'warning' && <SearchIcon fontSize="small" sx={{ color: '#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>
{/* Stift-Icon */}
<EditIcon
fontSize="small"
sx={{ color: '#555', cursor: 'pointer' }}
onClick={() => handleEditClick(row.value, index)}
/>
)}
</Box>
</TableCell>
{/* Seitenzahl */}
<TableCell>
<Link
component="button"
onClick={() => onPageClick?.(row.page)}
sx={{ cursor: 'pointer' }}
>
{row.page}
</Link>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
{/* Dialog zum Bearbeiten */}
<Dialog open={open} onClose={() => setOpen(false)}>
<DialogTitle>Kennzahl bearbeiten</DialogTitle>
<DialogContent>
<TextField
fullWidth
value={currentValue}
onChange={(e) => setCurrentValue(e.target.value)}
label="Neuer Wert"
variant="outlined"
margin="dense"
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)}>Abbrechen</Button>
<Button onClick={handleSave} variant="contained">Speichern</Button>
</DialogActions>
</Dialog>
</>
<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>
);
}