Merge branch 'main' into #54-async-ocr
# Conflicts: # project/backend/coordinator/app.pypull/57/head
commit
081aa0e936
|
|
@ -5,6 +5,7 @@ from dotenv import load_dotenv
|
||||||
from controller import register_routes
|
from controller import register_routes
|
||||||
from model.database import init_db
|
from model.database import init_db
|
||||||
from controller.socketIO import socketio
|
from controller.socketIO import socketio
|
||||||
|
from controller.kennzahlen import kennzahlen_bp
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
@ -21,6 +22,9 @@ init_db(app)
|
||||||
register_routes(app)
|
register_routes(app)
|
||||||
|
|
||||||
|
|
||||||
|
# Register blueprints
|
||||||
|
app.register_blueprint(kennzahlen_bp)
|
||||||
|
|
||||||
@app.route("/health")
|
@app.route("/health")
|
||||||
def health_check():
|
def health_check():
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -8,8 +8,8 @@ services:
|
||||||
image: postgres:17-alpine
|
image: postgres:17-alpine
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
# ports:
|
||||||
- "5432:5432"
|
# - "5432:5432"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U admin"]
|
test: ["CMD-SHELL", "pg_isready -U admin"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|
|
||||||
|
|
@ -1,145 +1,211 @@
|
||||||
import {
|
import {
|
||||||
Table, TableBody, TableCell, TableContainer,
|
Table, TableBody, TableCell, TableContainer,
|
||||||
TableHead, TableRow, Paper, Box,
|
TableHead, TableRow, Paper, Box,
|
||||||
Dialog, DialogActions, DialogContent, DialogTitle,
|
TextField, Link
|
||||||
TextField, Button, Link
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
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
|
interface Kennzahl {
|
||||||
const exampleData = [
|
pdf_id: string;
|
||||||
{ label: 'Fondsname', value: 'Fund Real Estate Prime Europe', page: 1, status: 'ok' },
|
label: string;
|
||||||
{ label: 'Fondsmanager', value: '', page: 1, status: 'error' },
|
value: string;
|
||||||
{ label: 'Risikoprofil', value: 'Core/Core+', page: 10, status: 'warning' },
|
page: number;
|
||||||
{ label: 'LTV', value: '30-35 %', page: 8, status: 'ok' },
|
status: 'ok' | 'error' | 'warning';
|
||||||
{ label: 'Ausschüttungsrendite', value: '4%', page: 34, status: 'ok' }
|
}
|
||||||
];
|
|
||||||
|
|
||||||
interface KennzahlenTableProps {
|
interface KennzahlenTableProps {
|
||||||
onPageClick?: (page: number) => void;
|
onPageClick?: (page: number) => void;
|
||||||
|
pdfId?: string; // Neue Prop für die PDF-ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// React-Komponente
|
// React-Komponente
|
||||||
export default function KennzahlenTable({ onPageClick }: KennzahlenTableProps) {
|
export default function KennzahlenTable({ onPageClick, pdfId = 'example' }: KennzahlenTableProps) {
|
||||||
// Zustand für bearbeitbare Daten
|
const [rows, setRows] = useState<Kennzahl[]>([]);
|
||||||
const [rows, setRows] = useState(exampleData);
|
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
|
// Initialisiere Beispieldaten
|
||||||
const [open, setOpen] = useState(false); // Dialog anzeigen?
|
const initializeData = async () => {
|
||||||
const [currentValue, setCurrentValue] = useState(''); // Eingabewert
|
try {
|
||||||
const [currentIndex, setCurrentIndex] = useState<number | null>(null); // Zeilenindex
|
const response = await fetch(`${API_BASE_URL}/api/kennzahlen/init`, {
|
||||||
|
method: 'POST'
|
||||||
// Beim Klick auf das Stift-Icon: Dialog öffnen
|
});
|
||||||
const handleEditClick = (value: string, index: number) => {
|
if (!response.ok) {
|
||||||
setCurrentValue(value);
|
throw new Error('Fehler beim Initialisieren der Daten');
|
||||||
setCurrentIndex(index);
|
}
|
||||||
setOpen(true);
|
// Lade die Daten nach der Initialisierung
|
||||||
};
|
await fetchKennzahlen();
|
||||||
|
} catch (err) {
|
||||||
// Wert speichern und Dialog schließen
|
setError('Fehler beim Initialisieren der Daten');
|
||||||
const handleSave = () => {
|
console.error('Fehler:', err);
|
||||||
if (currentIndex !== null) {
|
|
||||||
const updated = [...rows];
|
|
||||||
updated[currentIndex].value = currentValue;
|
|
||||||
setRows(updated);
|
|
||||||
}
|
}
|
||||||
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 (
|
return (
|
||||||
<>
|
<TableContainer component={Paper}>
|
||||||
<TableContainer component={Paper}>
|
<Table>
|
||||||
<Table>
|
<TableHead>
|
||||||
{/* Tabellenkopf */}
|
<TableRow>
|
||||||
<TableHead>
|
<TableCell><strong>Kennzahl</strong></TableCell>
|
||||||
<TableRow>
|
<TableCell><strong>Wert</strong></TableCell>
|
||||||
<TableCell><strong>Kennzahl</strong></TableCell>
|
<TableCell><strong>Seite</strong></TableCell>
|
||||||
<TableCell><strong>Wert</strong></TableCell>
|
</TableRow>
|
||||||
<TableCell><strong>Seite</strong></TableCell>
|
</TableHead>
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
|
|
||||||
{/* Tabelleninhalt */}
|
<TableBody>
|
||||||
<TableBody>
|
{rows.map((row, index) => {
|
||||||
{rows.map((row, index) => {
|
let borderColor = 'transparent';
|
||||||
// Rahmenfarbe anhand Status
|
if (row.status === 'error') borderColor = 'red';
|
||||||
let borderColor = 'transparent';
|
else if (row.status === 'warning') borderColor = '#f6ed48';
|
||||||
if (row.status === 'error') borderColor = 'red';
|
|
||||||
else if (row.status === 'warning') borderColor = '#f6ed48';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={index}>
|
<TableRow key={index}>
|
||||||
{/* Kennzahl */}
|
<TableCell>{row.label}</TableCell>
|
||||||
<TableCell>{row.label}</TableCell>
|
<TableCell onClick={() => startEditing(row.value, index)}>
|
||||||
|
<Box
|
||||||
{/* Wert mit Status-Icons + Stift rechts */}
|
sx={{
|
||||||
<TableCell>
|
border: `2px solid ${borderColor}`,
|
||||||
<Box
|
borderRadius: 1,
|
||||||
sx={{
|
padding: '4px 8px',
|
||||||
border: `2px solid ${borderColor}`,
|
display: 'flex',
|
||||||
borderRadius: 1,
|
alignItems: 'center',
|
||||||
padding: '4px 8px',
|
justifyContent: 'space-between',
|
||||||
display: 'flex',
|
width: '100%',
|
||||||
alignItems: 'center',
|
cursor: 'text',
|
||||||
justifyContent: 'space-between',
|
}}
|
||||||
width: '100%',
|
>
|
||||||
}}
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%' }}>
|
||||||
>
|
{row.status === 'error' && <ErrorOutlineIcon fontSize="small" color="error" />}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
{row.status === 'warning' && <SearchIcon fontSize="small" sx={{ color: '#f6ed48' }} />}
|
||||||
{row.status === 'error' && <ErrorOutlineIcon fontSize="small" color="error" />}
|
{editingIndex === index ? (
|
||||||
{row.status === 'warning' && <SearchIcon fontSize="small" sx={{ color: '#f6ed48' }} />}
|
<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>
|
<span>{row.value || '—'}</span>
|
||||||
</Box>
|
)}
|
||||||
|
|
||||||
{/* Stift-Icon */}
|
|
||||||
<EditIcon
|
|
||||||
fontSize="small"
|
|
||||||
sx={{ color: '#555', cursor: 'pointer' }}
|
|
||||||
onClick={() => handleEditClick(row.value, index)}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
</TableCell>
|
<EditIcon
|
||||||
|
fontSize="small"
|
||||||
{/* Seitenzahl */}
|
sx={{ color: '#555', cursor: 'pointer' }}
|
||||||
<TableCell>
|
onClick={(e) => {
|
||||||
<Link
|
e.stopPropagation();
|
||||||
component="button"
|
startEditing(row.value, index);
|
||||||
onClick={() => onPageClick?.(row.page)}
|
}}
|
||||||
sx={{ cursor: 'pointer' }}
|
/>
|
||||||
>
|
</Box>
|
||||||
{row.page}
|
</TableCell>
|
||||||
</Link>
|
<TableCell>
|
||||||
</TableCell>
|
<Link
|
||||||
</TableRow>
|
component="button"
|
||||||
);
|
onClick={() => onPageClick?.(row.page)}
|
||||||
})}
|
sx={{ cursor: 'pointer' }}
|
||||||
</TableBody>
|
>
|
||||||
</Table>
|
{row.page}
|
||||||
</TableContainer>
|
</Link>
|
||||||
|
</TableCell>
|
||||||
{/* Dialog zum Bearbeiten */}
|
</TableRow>
|
||||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
);
|
||||||
<DialogTitle>Kennzahl bearbeiten</DialogTitle>
|
})}
|
||||||
<DialogContent>
|
</TableBody>
|
||||||
<TextField
|
</Table>
|
||||||
fullWidth
|
</TableContainer>
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue