Compare commits

..

No commits in common. "c9de2cb027134b29302856f5f0ca0e9235cb75c0" and "09c314eea3ce3b6435de7f890ed4d9329823a8a4" have entirely different histories.

46 changed files with 452 additions and 12908 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,4 +1,4 @@
from controller.spacy_controller import spacy_controller from controller.spacy_contoller import spacy_controller
from controller.kpi_setting_controller import kpi_setting_controller from controller.kpi_setting_controller import kpi_setting_controller
from controller.pitch_book_controller import pitch_book_controller from controller.pitch_book_controller import pitch_book_controller
from controller.progress_controller import progress_controller from controller.progress_controller import progress_controller

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

@ -35,7 +35,7 @@ def create_kpi_setting():
"translation", "translation",
"example", "example",
"position", "position",
"active", "active"
] ]
for field in required_fields: for field in required_fields:
if field not in data: if field not in data:
@ -61,7 +61,7 @@ def create_kpi_setting():
translation=data["translation"], translation=data["translation"],
example=data["example"], example=data["example"],
position=data["position"], position=data["position"],
active=data["active"], active=data["active"]
) )
db.session.add(new_kpi_setting) db.session.add(new_kpi_setting)
@ -136,12 +136,7 @@ def update_kpi_positions():
try: try:
for update_item in data: for update_item in data:
if "id" not in update_item or "position" not in update_item: if "id" not in update_item or "position" not in update_item:
return ( return jsonify({"error": "Each item must have 'id' and 'position' fields"}), 400
jsonify(
{"error": "Each item must have 'id' and 'position' fields"}
),
400,
)
kpi_setting = KPISettingModel.query.get_or_404(update_item["id"]) kpi_setting = KPISettingModel.query.get_or_404(update_item["id"])
kpi_setting.position = update_item["position"] kpi_setting.position = update_item["position"]

View File

@ -19,6 +19,6 @@ def progress():
): ):
return jsonify({"error": "Invalid progress value"}), 400 return jsonify({"error": "Invalid progress value"}), 400
socketio.emit("progress", {"id": int(data["id"]), "progress": data["progress"]}) socketio.emit("progress", {"id": data["id"], "progress": data["progress"]})
# Process the data and return a response # Process the data and return a response
return jsonify({"message": "Progress updated"}) return jsonify({"message": "Progress updated"})

View File

@ -1,11 +1,10 @@
from flask import Blueprint, request, jsonify, send_file from flask import Blueprint, request, jsonify, send_file
from io import BytesIO from io import BytesIO
from model.spacy_model import SpacyModel from model.spacy_model import SpacyModel
import puremagic import puremagic
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from model.database import db from model.database import db
import os
import json
spacy_controller = Blueprint("spacy", __name__, url_prefix="/api/spacy") spacy_controller = Blueprint("spacy", __name__, url_prefix="/api/spacy")
@ -92,39 +91,3 @@ def delete_file(id):
db.session.commit() db.session.commit()
return jsonify({"message": f"File {id} deleted successfully"}), 200 return jsonify({"message": f"File {id} deleted successfully"}), 200
@spacy_controller.route("/append-training-entry", methods=["POST"])
def append_training_entry():
entry = request.get_json()
if not entry or "text" not in entry or "entities" not in entry:
return (
jsonify(
{"error": "Ungültiges Format 'text' und 'entities' erforderlich."}
),
400,
)
path = os.path.join("spacy_training", "annotation_data.json")
try:
os.makedirs(os.path.dirname(path), exist_ok=True)
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
else:
data = []
if entry in data:
return jsonify({"message": "Eintrag existiert bereits."}), 200
data.append(entry)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return jsonify({"message": "Eintrag erfolgreich gespeichert."}), 200
except Exception as e:
print(f"[ERROR] Fehler beim Schreiben: {e}")
return jsonify({"error": "Interner Fehler beim Schreiben."}), 500

View File

@ -14,5 +14,4 @@ def init_db(app):
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
from model.seed_data import seed_default_kpi_settings from model.seed_data import seed_default_kpi_settings
seed_default_kpi_settings() seed_default_kpi_settings()

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

@ -38,12 +38,10 @@ class KPISettingModel(db.Model):
"translation": self.translation, "translation": self.translation,
"example": self.example, "example": self.example,
"position": self.position, "position": self.position,
"active": self.active, "active": self.active
} }
def __init__( def __init__(self, name, description, mandatory, type, translation, example, position, active):
self, name, description, mandatory, type, translation, example, position, active
):
self.name = name self.name = name
self.description = description self.description = description
self.mandatory = mandatory self.mandatory = mandatory

View File

@ -1,7 +1,6 @@
from model.database import db from model.database import db
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import LargeBinary from sqlalchemy import LargeBinary
from datetime import datetime
class PitchBookModel(db.Model): class PitchBookModel(db.Model):
@ -9,15 +8,9 @@ class PitchBookModel(db.Model):
filename: Mapped[str] = mapped_column() filename: Mapped[str] = mapped_column()
file: Mapped[bytes] = mapped_column(LargeBinary) file: Mapped[bytes] = mapped_column(LargeBinary)
kpi: Mapped[str | None] kpi: Mapped[str | None]
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
def to_dict(self): def to_dict(self):
return { return {"id": self.id, "filename": self.filename, "kpi": self.kpi}
"id": self.id,
"filename": self.filename,
"kpi": self.kpi,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
def __init__(self, filename, file): def __init__(self, filename, file):
self.filename = filename self.filename = filename

View File

@ -1,7 +1,6 @@
from model.database import db from model.database import db
from model.kpi_setting_model import KPISettingModel, KPISettingType from model.kpi_setting_model import KPISettingModel, KPISettingType
def seed_default_kpi_settings(): def seed_default_kpi_settings():
if KPISettingModel.query.first() is not None: if KPISettingModel.query.first() is not None:
print("KPI Settings bereits vorhanden, Seeding übersprungen") print("KPI Settings bereits vorhanden, Seeding übersprungen")
@ -16,7 +15,7 @@ def seed_default_kpi_settings():
"translation": "Fund Name", "translation": "Fund Name",
"example": "Alpha Real Estate Fund I", "example": "Alpha Real Estate Fund I",
"position": 1, "position": 1,
"active": True, "active": True
}, },
{ {
"name": "Fondsmanager", "name": "Fondsmanager",
@ -26,7 +25,7 @@ def seed_default_kpi_settings():
"translation": "Fund Manager", "translation": "Fund Manager",
"example": "Max Mustermann", "example": "Max Mustermann",
"position": 2, "position": 2,
"active": True, "active": True
}, },
{ {
"name": "AIFM", "name": "AIFM",
@ -36,7 +35,7 @@ def seed_default_kpi_settings():
"translation": "AIFM", "translation": "AIFM",
"example": "Alpha Investment Management GmbH", "example": "Alpha Investment Management GmbH",
"position": 3, "position": 3,
"active": True, "active": True
}, },
{ {
"name": "Datum", "name": "Datum",
@ -46,7 +45,7 @@ def seed_default_kpi_settings():
"translation": "Date", "translation": "Date",
"example": "05.05.2025", "example": "05.05.2025",
"position": 4, "position": 4,
"active": True, "active": True
}, },
{ {
"name": "Risikoprofil", "name": "Risikoprofil",
@ -56,7 +55,7 @@ def seed_default_kpi_settings():
"translation": "Risk Profile", "translation": "Risk Profile",
"example": "Core/Core++", "example": "Core/Core++",
"position": 5, "position": 5,
"active": True, "active": True
}, },
{ {
"name": "Artikel", "name": "Artikel",
@ -66,7 +65,7 @@ def seed_default_kpi_settings():
"translation": "Article", "translation": "Article",
"example": "Artikel 8", "example": "Artikel 8",
"position": 6, "position": 6,
"active": True, "active": True
}, },
{ {
"name": "Zielrendite", "name": "Zielrendite",
@ -76,7 +75,7 @@ def seed_default_kpi_settings():
"translation": "Target Return", "translation": "Target Return",
"example": "6.5", "example": "6.5",
"position": 7, "position": 7,
"active": True, "active": True
}, },
{ {
"name": "Rendite", "name": "Rendite",
@ -86,7 +85,7 @@ def seed_default_kpi_settings():
"translation": "Return", "translation": "Return",
"example": "5.8", "example": "5.8",
"position": 8, "position": 8,
"active": True, "active": True
}, },
{ {
"name": "Zielausschüttung", "name": "Zielausschüttung",
@ -96,7 +95,7 @@ def seed_default_kpi_settings():
"translation": "Target Distribution", "translation": "Target Distribution",
"example": "4.0", "example": "4.0",
"position": 9, "position": 9,
"active": True, "active": True
}, },
{ {
"name": "Ausschüttung", "name": "Ausschüttung",
@ -106,7 +105,7 @@ def seed_default_kpi_settings():
"translation": "Distribution", "translation": "Distribution",
"example": "3.8", "example": "3.8",
"position": 10, "position": 10,
"active": True, "active": True
}, },
{ {
"name": "Laufzeit", "name": "Laufzeit",
@ -116,7 +115,7 @@ def seed_default_kpi_settings():
"translation": "Duration", "translation": "Duration",
"example": "7 Jahre, 10, Evergreen", "example": "7 Jahre, 10, Evergreen",
"position": 11, "position": 11,
"active": True, "active": True
}, },
{ {
"name": "LTV", "name": "LTV",
@ -126,7 +125,7 @@ def seed_default_kpi_settings():
"translation": "LTV", "translation": "LTV",
"example": "65.0", "example": "65.0",
"position": 12, "position": 12,
"active": True, "active": True
}, },
{ {
"name": "Managementgebühren", "name": "Managementgebühren",
@ -136,7 +135,7 @@ def seed_default_kpi_settings():
"translation": "Management Fees", "translation": "Management Fees",
"example": "1.5", "example": "1.5",
"position": 13, "position": 13,
"active": True, "active": True
}, },
{ {
"name": "Sektorenallokation", "name": "Sektorenallokation",
@ -146,7 +145,7 @@ def seed_default_kpi_settings():
"translation": "Sector Allocation", "translation": "Sector Allocation",
"example": "Büro, Wohnen, Logistik, Studentenwohnen", "example": "Büro, Wohnen, Logistik, Studentenwohnen",
"position": 14, "position": 14,
"active": True, "active": True
}, },
{ {
"name": "Länderallokation", "name": "Länderallokation",
@ -156,8 +155,8 @@ def seed_default_kpi_settings():
"translation": "Country Allocation", "translation": "Country Allocation",
"example": "Deutschland,Frankreich, Österreich, Schweiz", "example": "Deutschland,Frankreich, Österreich, Schweiz",
"position": 15, "position": 15,
"active": True, "active": True
}, }
] ]
print("Füge Standard KPI Settings hinzu...") print("Füge Standard KPI Settings hinzu...")
@ -171,16 +170,14 @@ def seed_default_kpi_settings():
translation=kpi_data["translation"], translation=kpi_data["translation"],
example=kpi_data["example"], example=kpi_data["example"],
position=kpi_data["position"], position=kpi_data["position"],
active=kpi_data["active"], active=kpi_data["active"]
) )
db.session.add(kpi_setting) db.session.add(kpi_setting)
try: try:
db.session.commit() db.session.commit()
print( print(f"Erfolgreich {len(default_kpi_settings)} Standard KPI Settings hinzugefügt")
f"Erfolgreich {len(default_kpi_settings)} Standard KPI Settings hinzugefügt"
)
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
print(f"Fehler beim Hinzufügen der Standard KPI Settings: {e}") print(f"Fehler beim Hinzufügen der Standard KPI Settings: {e}")

View File

@ -3,19 +3,12 @@ from extractSpacy import extract
import requests import requests
import os import os
import json import json
from flask_cors import CORS
app = Flask(__name__) app = Flask(__name__)
CORS(app)
VALIDATE_SERVICE_URL = os.getenv("VALIDATE_SERVICE_URL", "http://localhost:5054/validate")
VALIDATE_SERVICE_URL = os.getenv( @app.route('/extract', methods=['POST'])
"VALIDATE_SERVICE_URL", "http://localhost:5054/validate"
)
@app.route("/extract", methods=["POST"])
def extract_pdf(): def extract_pdf():
json_data = request.get_json() json_data = request.get_json()
@ -23,19 +16,19 @@ def extract_pdf():
pages_data = json_data["extracted_text_per_page"] pages_data = json_data["extracted_text_per_page"]
entities_json = extract(pages_data) entities_json = extract(pages_data)
entities = ( entities = json.loads(entities_json) if isinstance(entities_json, str) else entities_json
json.loads(entities_json) if isinstance(entities_json, str) else entities_json
)
validate_payload = {"id": pitchbook_id, "service": "spacy", "entities": entities} validate_payload = {
"id": pitchbook_id,
"service": "spacy",
"entities": entities
}
print(f"[SPACY] Sending to validate service: {VALIDATE_SERVICE_URL}") print(f"[SPACY] Sending to validate service: {VALIDATE_SERVICE_URL}")
print(f"[SPACY] Payload: {validate_payload} entities for pitchbook {pitchbook_id}") print(f"[SPACY] Payload: {validate_payload} entities for pitchbook {pitchbook_id}")
try: try:
response = requests.post( response = requests.post(VALIDATE_SERVICE_URL, json=validate_payload, timeout=600)
VALIDATE_SERVICE_URL, json=validate_payload, timeout=600
)
print(f"[SPACY] Validate service response: {response.status_code}") print(f"[SPACY] Validate service response: {response.status_code}")
if response.status_code != 200: if response.status_code != 200:
print(f"[SPACY] Validate service error: {response.text}") print(f"[SPACY] Validate service error: {response.text}")
@ -45,40 +38,5 @@ def extract_pdf():
return jsonify("Sent to validate-service"), 200 return jsonify("Sent to validate-service"), 200
@app.route("/append-training-entry", methods=["POST"])
def append_training_entry():
entry = request.get_json()
if not entry or "text" not in entry or "entities" not in entry:
return (
jsonify(
{"error": "Ungültiges Format 'text' und 'entities' erforderlich."}
),
400,
)
path = os.path.join("spacy_training", "annotation_data.json")
try:
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
else:
data = []
# Optional: Duplikate prüfen
if entry in data:
return jsonify({"message": "Eintrag existiert bereits."}), 200
data.append(entry)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return jsonify({"message": "Eintrag erfolgreich gespeichert."}), 200
except Exception as e:
print(f"[ERROR] Fehler beim Schreiben der Datei: {e}")
return jsonify({"error": "Interner Fehler beim Schreiben."}), 500
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", port=5052, debug=True) app.run(host="0.0.0.0", port=5052, debug=True)

View File

@ -1,35 +0,0 @@
from flask import Flask, request, jsonify
import os
import json
app = Flask(__name__)
ANNOTATION_FILE = (
"spacy_training/annotation_data.json" # relativer Pfad im Container/Projekt
)
@app.route("/api/spacy-training-entry", methods=["POST"])
def append_training_entry():
new_entry = request.get_json()
if not new_entry or "text" not in new_entry or "entities" not in new_entry:
return jsonify({"error": "Ungültiges Format"}), 400
# Bestehende Datei laden
if os.path.exists(ANNOTATION_FILE):
with open(ANNOTATION_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
else:
data = []
# Optional: Duplikat vermeiden
if new_entry in data:
return jsonify({"message": "Eintrag bereits vorhanden."}), 200
# Anfügen
data.append(new_entry)
with open(ANNOTATION_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return jsonify({"message": "Eintrag erfolgreich gespeichert."}), 200

View File

@ -1,130 +0,0 @@
[paths]
train = null
dev = null
vectors = null
init_tok2vec = null
[system]
seed = 0
gpu_allocator = null
[nlp]
lang = "de"
pipeline = ["ner"]
disabled = []
before_creation = null
after_creation = null
after_pipeline_creation = null
batch_size = 1000
tokenizer = {"@tokenizers":"spacy.Tokenizer.v1"}
vectors = {"@vectors":"spacy.Vectors.v1"}
[components]
[components.ner]
factory = "ner"
incorrect_spans_key = null
moves = null
scorer = {"@scorers":"spacy.ner_scorer.v1"}
update_with_oracle_cut_size = 100
[components.ner.model]
@architectures = "spacy.TransitionBasedParser.v2"
state_type = "ner"
extra_state_tokens = false
hidden_width = 64
maxout_pieces = 2
use_upper = true
nO = null
[components.ner.model.tok2vec]
@architectures = "spacy.HashEmbedCNN.v2"
pretrained_vectors = null
width = 96
depth = 4
embed_size = 2000
window_size = 1
maxout_pieces = 3
subword_features = true
[corpora]
[corpora.dev]
@readers = "spacy.Corpus.v1"
path = ${paths.dev}
gold_preproc = false
max_length = 0
limit = 0
augmenter = null
[corpora.train]
@readers = "spacy.Corpus.v1"
path = ${paths.train}
gold_preproc = false
max_length = 0
limit = 0
augmenter = null
[training]
seed = ${system.seed}
gpu_allocator = ${system.gpu_allocator}
dropout = 0.1
accumulate_gradient = 1
patience = 1600
max_epochs = 0
max_steps = 20000
eval_frequency = 200
frozen_components = []
annotating_components = []
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
before_to_disk = null
before_update = null
[training.batcher]
@batchers = "spacy.batch_by_words.v1"
discard_oversize = false
tolerance = 0.2
get_length = null
[training.batcher.size]
@schedules = "compounding.v1"
start = 100
stop = 1000
compound = 1.001
t = 0.0
[training.logger]
@loggers = "spacy.ConsoleLogger.v1"
progress_bar = false
[training.optimizer]
@optimizers = "Adam.v1"
beta1 = 0.9
beta2 = 0.999
L2_is_weight_decay = true
L2 = 0.01
grad_clip = 1.0
use_averages = false
eps = 0.00000001
learn_rate = 0.001
[training.score_weights]
ents_f = 1.0
ents_p = 0.0
ents_r = 0.0
ents_per_type = null
[pretraining]
[initialize]
vectors = ${paths.vectors}
init_tok2vec = ${paths.init_tok2vec}
vocab_data = null
lookups = null
before_init = null
after_init = null
[initialize.components]
[initialize.tokenizer]

View File

@ -1,63 +0,0 @@
{
"lang":"de",
"name":"pipeline",
"version":"0.0.0",
"spacy_version":">=3.8.7,<3.9.0",
"description":"",
"author":"",
"email":"",
"url":"",
"license":"",
"spacy_git_version":"4b65aa7",
"vectors":{
"width":0,
"vectors":0,
"keys":0,
"name":null,
"mode":"default"
},
"labels":{
"ner":[
"AUSSCH\u00dcTTUNGSRENDITE",
"ESFCG",
"HHGK",
"KENNZAHL",
"LAUFZEIT",
"LAUFZEIT2",
"L\u00c4NDERALLOKATION",
"MANAGMENTGEB\u00dcHREN",
"RENDITE",
"RISIKOPROFIL",
"SEKTORENALLOKATION",
"TEST",
"TEST3",
"TEST323",
"TEST33",
"TEST34",
"TEST345",
"TEST35",
"TEST36",
"TEST37",
"TEST4",
"TEST44",
"TEST45",
"TEST5",
"TEST65",
"TEST67",
"TEST88",
"TEST99",
"ZIELAUSSCH\u00dcTTUNG",
"ZIELRENDITE",
"ZZGER 33"
]
},
"pipeline":[
"ner"
],
"components":[
"ner"
],
"disabled":[
]
}

View File

@ -1,13 +0,0 @@
{
"moves":null,
"update_with_oracle_cut_size":100,
"multitasks":[
],
"min_action_freq":1,
"learn_tokens":false,
"beam_width":1,
"beam_density":0.0,
"beam_update_prob":0.0,
"incorrect_spans_key":null
}

View File

@ -1 +0,0 @@
¥movesÚÈ{"0":{},"1":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"2":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"3":{"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"4":{"":1,"KENNZAHL":-1,"RISIKOPROFIL":-2,"AUSSCH\u00dcTTUNGSRENDITE":-3,"LAUFZEIT":-4,"RENDITE":-5,"L\u00c4NDERALLOKATION":-6,"ZIELRENDITE":-7,"ZIELAUSSCH\u00dcTTUNG":-8,"MANAGMENTGEB\u00dcHREN":-9,"SEKTORENALLOKATION":-10,"LAUFZEIT2":-11,"TEST":-12,"TEST3":-13,"TEST4":-14,"TEST5":-15,"TEST34":-16,"TEST35":-17,"TEST36":-18,"TEST37":-19,"HHGK":-20,"TEST67":-21,"ESFCG":-22,"ZZGER 33":-23,"TEST65":-24,"TEST99":-25,"TEST88":-26,"TEST44":-27,"TEST33":-28,"TEST345":-29,"TEST45":-30,"TEST323":-31},"5":{"":1}}£cfg<66>§neg_keyÀ

File diff suppressed because one or more lines are too long

View File

@ -1,122 +0,0 @@
[
{
"text": "Core",
"entities": [
[
0,
4,
"RISIKOPROFIL"
]
]
},
{
"text": "Core+",
"entities": [
[
0,
5,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Core+",
"entities": [
[
0,
10,
"RISIKOPROFIL"
]
]
},
{
"text": "Value Add",
"entities": [
[
0,
9,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Value Add",
"entities": [
[
0,
14,
"RISIKOPROFIL"
]
]
},
{
"text": "Core+/Value Add",
"entities": [
[
0,
15,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Core+/Value Add",
"entities": [
[
0,
20,
"RISIKOPROFIL"
]
]
},
{
"text": "The RE portfolio of the fund is a good illustration of Fond expertise in European core/core+ investments .",
"entities": [
[
82,
92,
"RISIKOPROFIL"
]
]
},
{
"text": "Risk level: Core/Core+",
"entities": [
[
12,
22,
"RISIKOPROFIL"
]
]
},
{
"text": "Different risk profile (core, core+, value-added)",
"entities": [
[
24,
48,
"RISIKOPROFIL"
]
]
},
{
"text": "Core/Core+ with OpCo premium",
"entities": [
[
0,
10,
"RISIKOPROFIL"
]
]
},
{
"text": "Core /Core+ Assets, well-established = Key Gateway Cities in Europe le.g. hotels in the market with minor asset London, Paris, Amsterdam, Berlin] management initiatives",
"entities": [
[
0,
11,
"RISIKOPROFIL"
]
]
}
]

View File

@ -37,8 +37,6 @@ services:
retries: 10 retries: 10
ports: ports:
- 5050:5000 - 5050:5000
volumes:
- ./backend/spacy-service/spacy_training:/app/spacy_training
ocr: ocr:
build: build:

View File

@ -4,9 +4,7 @@ WORKDIR /usr/src/app
# install dependencies into temp directory # install dependencies into temp directory
# this will cache them and speed up future builds # this will cache them and speed up future builds
COPY package.json bun.lockb ./ COPY package.json bun.lockb ./
#RUN bun install --frozen-lockfile RUN bun install --frozen-lockfile
RUN bun install
COPY . . COPY . .

View File

@ -3,14 +3,15 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico?v=1" /> <link rel="icon" href="/favicon.ico" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="Web site created using create-tsrouter-app" content="Web site created using create-tsrouter-app"
/> />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<title>Pitchbook Extractor</title> <title>Create TanStack App - frontend</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,6 @@
"@tanstack/react-router": "^1.114.3", "@tanstack/react-router": "^1.114.3",
"@tanstack/react-router-devtools": "^1.114.3", "@tanstack/react-router-devtools": "^1.114.3",
"@tanstack/router-plugin": "^1.114.3", "@tanstack/router-plugin": "^1.114.3",
"file-saver": "^2.0.5",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-material-file-upload": "^0.0.4", "react-material-file-upload": "^0.0.4",
@ -34,7 +33,6 @@
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0", "@testing-library/react": "^16.2.0",
"@types/file-saver": "^2.0.7",
"@types/react": "^19.0.8", "@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3", "@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View File

@ -3,7 +3,6 @@ import { Box, Typography, Button, Paper, TextField, FormControlLabel,
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import type { Kennzahl } from "../types/kpi"; import type { Kennzahl } from "../types/kpi";
import { typeDisplayMapping } from "../types/kpi"; import { typeDisplayMapping } from "../types/kpi";
// import { saveAs } from "file-saver";
interface KPIFormProps { interface KPIFormProps {
mode: 'add' | 'edit'; mode: 'add' | 'edit';
@ -20,9 +19,7 @@ const emptyKPI: Partial<Kennzahl> = {
type: 'string', type: 'string',
translation: '', translation: '',
example: '', example: '',
active: true, active: true
exampleText: '',
markedValue: '',
}; };
export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }: KPIFormProps) { export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }: KPIFormProps) {
@ -43,60 +40,16 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
return; return;
} }
if (!formData.exampleText?.trim()) {
alert('Beispielsatz ist erforderlich');
return;
}
if (!formData.markedValue?.trim()) {
alert('Bezeichneter Wert im Satz ist erforderlich');
return;
}
setIsSaving(true); setIsSaving(true);
try { try {
const spacyEntry = generateSpacyEntry(formData);
//in localStorage merken
const stored = localStorage.getItem("spacyData");
const existingData = stored ? JSON.parse(stored) : [];
const updated = [...existingData, spacyEntry];
localStorage.setItem("spacyData", JSON.stringify(updated));
// an Flask senden
const response = await fetch("http://localhost:5050/api/spacy/append-training-entry", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(spacyEntry)
});
const data = await response.json();
console.log("Response von /append-training-entry:", data);
if (!response.ok) {
throw new Error(data.error || "Fehler beim Aufruf von append-training-entry");
}
if (!response.ok) {
throw new Error("Fehler vom Backend: " + response.status);
}
// anschließend in der Datenbank speichern
await onSave(formData); await onSave(formData);
} catch (error) {
alert("SpaCy-Eintrag erfolgreich gespeichert!"); console.error('Error saving KPI:', error);
} catch (e: any) {
alert(e.message || "Fehler beim Erzeugen des Trainingsbeispiels.");
console.error(e);
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
}; };
const handleCancel = () => { const handleCancel = () => {
onCancel(); onCancel();
}; };
@ -153,40 +106,18 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
<Box mb={4}> <Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}> <Typography variant="h6" fontWeight="bold" mb={2}>
Beispielsatz Beschreibung
</Typography> </Typography>
<TextField <TextField
fullWidth fullWidth
multiline multiline
rows={3} rows={3}
label="Beispielsatz" label="Beschreibung"
required value={formData.description || ''}
value={formData.exampleText || ''} onChange={(e) => updateField('description', e.target.value)}
onChange={(e) => updateField('exampleText', e.target.value)} helperText="Beschreibung der Kennzahl"
error={!formData.exampleText?.trim()}
helperText={
!formData.exampleText?.trim()
? "Beispielsatz ist erforderlich"
: "Ein vollständiger Satz, in dem der markierte Begriff vorkommt"
}
/> />
<TextField
fullWidth
required
sx={{ mt: 2 }}
label="Bezeichneter Wert im Satz *"
value={formData.markedValue || ''}
onChange={(e) => updateField('markedValue', e.target.value)}
error={!formData.markedValue?.trim()}
helperText={
!formData.markedValue?.trim()
? "Markierter Begriff ist erforderlich"
: "Nur der Begriff, der im Satz markiert werden soll (z.B. Core/Core+)"
}
/>
<Box mt={3}> <Box mt={3}>
<FormControlLabel <FormControlLabel
control={ control={
@ -314,27 +245,3 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
</Paper> </Paper>
); );
} }
function generateSpacyEntry(formData: Partial<Kennzahl>) {
const text = formData.exampleText?.trim() || "";
const value = formData.markedValue?.trim() || "";
const label = formData.name?.trim().toUpperCase() || "";
const start = text.indexOf(value);
if (start === -1) {
throw new Error("Bezeichneter Begriff wurde im Satz nicht gefunden.");
}
return {
text,
entities: [[start, start + value.length, label]],
};
}
// function appendAndDownload(newEntry: any, existing: any[] = []) {
// const updated = [...existing, newEntry];
// const blob = new Blob([JSON.stringify(updated, null, 2)], {
// type: "application/json",
// });
// saveAs(blob, "..\project\backend\spacy-service\spacy_training\annotation_data.json");
// }

View File

@ -42,43 +42,25 @@ export default function KennzahlenTable({
data, data,
pdfId, pdfId,
settings, settings,
from, from
}: KennzahlenTableProps) { }: KennzahlenTableProps) {
const [editingIndex, setEditingIndex] = useState<string>(""); const [editingIndex, setEditingIndex] = useState<string>("");
const [editValue, setEditValue] = useState(""); const [editValue, setEditValue] = useState("");
const [editingPageIndex, setEditingPageIndex] = useState<string>("");
const [editPageValue, setEditPageValue] = useState("");
const [hoveredPageIndex, setHoveredPageIndex] = useState<string>("");
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { mutate } = useMutation({ const { mutate } = useMutation({
mutationFn: (params: { id: string; newValue?: string; newPage?: number }) => { mutationFn: (id: string) => {
const { id, newValue, newPage } = params;
const key = id.toUpperCase(); const key = id.toUpperCase();
const updatedData = { ...data }; const updatedData = { ...data };
updatedData[key] = data[key]?.map((item) => ({
if (data[key] && data[key].length > 0) { ...item,
updatedData[key] = data[key].map((item) => ({ entity: editValue,
...item, })) || [{ label: key, entity: editValue }];
...(newValue !== undefined && { entity: newValue }),
...(newPage !== undefined && { page: newPage }),
}));
} else {
updatedData[key] = [{
label: key,
entity: newValue || "",
page: newPage || 0,
status: "single-source",
source: "manual"
}];
}
return fetchPutKPI(Number(pdfId), updatedData); return fetchPutKPI(Number(pdfId), updatedData);
}, },
onMutate: async (params: { id: string; newValue?: string; newPage?: number }) => { onMutate: async (id: string) => {
const { id, newValue, newPage } = params;
await queryClient.cancelQueries({ await queryClient.cancelQueries({
queryKey: ["pitchBookKPI", pdfId], queryKey: ["pitchBookKPI", pdfId],
}); });
@ -89,23 +71,10 @@ export default function KennzahlenTable({
queryClient.setQueryData(["pitchBookKPI", pdfId], () => { queryClient.setQueryData(["pitchBookKPI", pdfId], () => {
const updatedData = { ...data }; const updatedData = { ...data };
updatedData[key] = data[key]?.map((item) => ({
if (data[key] && data[key].length > 0) { ...item,
updatedData[key] = data[key].map((item) => ({ entity: editValue,
...item, })) || [{ label: key, entity: editValue }];
...(newValue !== undefined && { entity: newValue }),
...(newPage !== undefined && { page: newPage }),
}));
} else {
updatedData[key] = [{
label: key,
entity: newValue || "",
page: newPage || 0,
status: "single-source",
source: "manual"
}];
}
return updatedData; return updatedData;
}); });
@ -130,41 +99,19 @@ export default function KennzahlenTable({
setEditValue(value); setEditValue(value);
}; };
const startPageEditing = (value: number, index: string) => {
setEditingPageIndex(index);
setEditPageValue(value.toString());
};
// Bearbeitung beenden und Wert speichern // Bearbeitung beenden und Wert speichern
const handleSave = async (index: string) => { const handleSave = async (index: string) => {
mutate({ id: index, newValue: editValue }); // await updateKennzahl(rows[index].label, editValue);
mutate(index);
setEditingIndex(""); setEditingIndex("");
}; };
const handlePageSave = async (index: string) => {
const pageNumber = parseInt(editPageValue);
if (editPageValue === "" || pageNumber === 0) {
mutate({ id: index, newPage: 0 });
} else if (!isNaN(pageNumber) && pageNumber > 0) {
mutate({ id: index, newPage: pageNumber });
}
setEditingPageIndex("");
};
// Tastatureingaben verarbeiten // Tastatureingaben verarbeiten
const handleKeyPress = (e: KeyboardEvent<HTMLDivElement>, index: string) => { const handleKeyPress = (e: KeyboardEvent<HTMLDivElement>, index: string) => {
if (e.key === "Enter") { if (e.key === "Enter") {
handleSave(index); handleSave(index);
} else if (e.key === "Escape") { } else if (e.key === "Escape") {
setEditingIndex(""); setEditingIndex("null");
}
};
const handlePageKeyPress = (e: KeyboardEvent<HTMLDivElement>, index: string) => {
if (e.key === "Enter") {
handlePageSave(index);
} else if (e.key === "Escape") {
setEditingPageIndex("");
} }
}; };
@ -184,16 +131,14 @@ export default function KennzahlenTable({
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell width="30%"> <TableCell>
<strong>Kennzahl</strong> <strong>Kennzahl</strong>
</TableCell> </TableCell>
<TableCell width="55%"> <TableCell>
<strong>Wert</strong> <strong>Wert</strong>
</TableCell> </TableCell>
<TableCell align="center" width="15%"> <TableCell align="center">
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 1 }}> <strong>Seite</strong>
<strong>Seite</strong>
</Box>
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
@ -220,17 +165,9 @@ export default function KennzahlenTable({
borderColor = "#f6ed48"; borderColor = "#f6ed48";
} }
const currentPage = row.extractedValues.at(0)?.page ?? 0;
const isPageHovered = hoveredPageIndex === row.setting.name;
const canEditPage = !hasMultipleValues;
return ( return (
<TableRow key={row.setting.name}> <TableRow key={row.setting.name}>
<TableCell>{row.setting.name} <TableCell>{row.setting.name}</TableCell>
{row.setting.mandatory && (
<span> *</span>
)}
</TableCell>
<TableCell <TableCell
onClick={() => { onClick={() => {
// Only allow inline editing for non-multiple value cells // Only allow inline editing for non-multiple value cells
@ -292,17 +229,12 @@ export default function KennzahlenTable({
</Tooltip> </Tooltip>
) : ( ) : (
<Tooltip <Tooltip
title={ title={hasNoValue ?
hasNoValue ? ( <>
<> <b>Problem</b>
<b>Problem</b> <br />
<br /> Es wurden keine Kennzahlen gefunden. Bitte ergänzen!
Es wurden keine Kennzahlen gefunden. Bitte </> : ""
ergänzen!
</>
) : (
""
)
} }
placement="bottom" placement="bottom"
arrow arrow
@ -329,10 +261,7 @@ export default function KennzahlenTable({
}} }}
> >
{hasNoValue && ( {hasNoValue && (
<ErrorOutlineIcon <ErrorOutlineIcon fontSize="small" color="error" />
fontSize="small"
color="error"
/>
)} )}
{editingIndex === row.setting.name ? ( {editingIndex === row.setting.name ? (
<TextField <TextField
@ -371,99 +300,21 @@ export default function KennzahlenTable({
)} )}
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
{editingPageIndex === row.setting.name ? ( {(row.extractedValues.at(0)?.page ?? 0) > 0 ? (
<TextField <Link
value={editPageValue} component="button"
onChange={(e) => { onClick={() => {
const value = e.target.value; const extractedValue = row.extractedValues.at(0);
if (value === '' || /^\d+$/.test(value)) { if (extractedValue?.page && extractedValue.page > 0) {
setEditPageValue(value); onPageClick?.(Number(extractedValue.page), extractedValue.entity || "");
} }
}} }}
onKeyDown={(e) => handlePageKeyPress(e, row.setting.name)} sx={{ cursor: "pointer" }}
onBlur={() => handlePageSave(row.setting.name)} >
autoFocus {row.extractedValues.at(0)?.page}
size="small" </Link>
variant="standard"
sx={{
width: "60px",
"& .MuiInput-input": {
textAlign: "center"
}
}}
inputProps={{
min: 0,
style: { textAlign: 'center' }
}}
/>
) : ( ) : (
<> ""
{currentPage > 0 ? (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
cursor: "pointer",
borderRadius: "4px",
minHeight: "32px",
minWidth: "100px",
}}
onClick={() => {
if (canEditPage) {
startPageEditing(currentPage, row.setting.name);
}
}}
>
<Link
component="button"
onClick={(e) => {
e.stopPropagation();
const extractedValue = row.extractedValues.at(0);
if (extractedValue?.page && extractedValue.page > 0) {
onPageClick?.(Number(extractedValue.page), extractedValue.entity || "");
}
}}
sx={{ cursor: "pointer" }}
>
{currentPage}
</Link>
</Box>
) : canEditPage ? (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
cursor: "pointer",
minHeight: "32px",
minWidth: "100px",
borderRadius: "4px",
backgroundColor: isPageHovered ? "#f8f9fa" : "transparent",
}}
onMouseEnter={() => setHoveredPageIndex(row.setting.name)}
onMouseLeave={() => setHoveredPageIndex("")}
onClick={() => startPageEditing(0, row.setting.name)}
>
<span style={{ color: "#999" }}>...</span>
<EditIcon
fontSize="small"
sx={{
position: "absolute",
left: "70px",
color: "black",
cursor: "pointer",
opacity: 0.7,
transition: "opacity 0.2s ease",
}}
/>
</Box>
) : (
""
)}
</>
)} )}
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

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

View File

@ -1,12 +1,11 @@
import SettingsIcon from "@mui/icons-material/Settings"; import SettingsIcon from "@mui/icons-material/Settings";
import { Backdrop, Box, Button, IconButton, Paper, Typography } from "@mui/material"; import { Backdrop, Box, Button, IconButton, Paper } from "@mui/material";
import { useNavigate, useRouter } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import FileUpload from "react-material-file-upload"; import FileUpload from "react-material-file-upload";
import { socket } from "../socket"; import { socket } from "../socket";
import { API_HOST } from "../util/api";
import { CircularProgressWithLabel } from "./CircularProgressWithLabel"; import { CircularProgressWithLabel } from "./CircularProgressWithLabel";
import DekaLogo from "../assets/Deka_logo.png"; import { API_HOST } from "../util/api";
export default function UploadPage() { export default function UploadPage() {
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<File[]>([]);
@ -14,7 +13,6 @@ export default function UploadPage() {
const [loadingState, setLoadingState] = useState<number | null>(null); const [loadingState, setLoadingState] = useState<number | null>(null);
const fileTypes = ["pdf"]; const fileTypes = ["pdf"];
const navigate = useNavigate(); const navigate = useNavigate();
const router = useRouter();
const uploadFile = useCallback(async () => { const uploadFile = useCallback(async () => {
const formData = new FormData(); const formData = new FormData();
@ -88,50 +86,26 @@ export default function UploadPage() {
display="flex" display="flex"
flexDirection="column" flexDirection="column"
alignItems="center" alignItems="center"
justifyContent="flex-start" justifyContent="center"
height="100vh" height="100vh"
bgcolor="white" bgcolor="white"
pt={3}
> >
<Box <Box
width="100%" width="100%"
maxWidth="1300px"
display="flex" display="flex"
justifyContent="space-between" justifyContent="flex-end"
alignItems="center" px={2}
px={8}
py={5}
> >
<Box sx={{ display: "flex", alignItems: "center" }}>
<img
src={DekaLogo}
alt="Company Logo"
style={{ height: "40px", width: "auto" }}
/>
</Box>
<IconButton onClick={() => navigate({ to: "/config" })}> <IconButton onClick={() => navigate({ to: "/config" })}>
<SettingsIcon fontSize="large" /> <SettingsIcon fontSize="large" />
</IconButton> </IconButton>
</Box> </Box>
<Typography
variant="h4"
component="h1"
sx={{
fontWeight: "bold",
color: "#383838",
marginBottom: 12,
marginTop: 6,
}}
>
Pitchbook Extractor
</Typography>
<Paper <Paper
elevation={3} elevation={3}
sx={{ sx={{
width: 800, width: 900,
height: 400, height: 500,
backgroundColor: "#eeeeee", backgroundColor: "#eeeeee",
borderRadius: 4, borderRadius: 4,
display: "flex", display: "flex",
@ -204,7 +178,6 @@ export default function UploadPage() {
backgroundColor: "#383838", backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" }, "&:hover": { backgroundColor: "#2e2e2e" },
}} }}
onMouseEnter={() => router.preloadRoute({ to: "/pitchbooks" })}
onClick={() => navigate({ to: "/pitchbooks" })} onClick={() => navigate({ to: "/pitchbooks" })}
> >
Alle Pitch Books anzeigen Alle Pitch Books anzeigen

View File

@ -95,78 +95,53 @@ export default function PDFViewer({
useEffect(() => { useEffect(() => {
const tmpPos: string[] = []; const tmpPos: string[] = [];
const tmpPosHighlight: string[] = []; const tmpPosHighlight: string[] = [];
const textItems = textContent.filter(
(e) => e.text !== "" && e.text !== " ",
);
if (textContent.length === 0) { textItems.forEach((e, i) => {
setPosHighlight([]); for (const s of highlight
setPosHighlightFocus([]); .filter((h) => h.page === pageNumber)
return; .map((h) => h.text)) {
} if (s.split(" ")[0] === e.text) {
const findTextPositions = (searchText: string): number[] => { if (
const positions: number[] = []; s.split(" ").reduce((prev, curr, j) => {
const normalizedSearch = searchText.toLowerCase().trim(); return prev && curr === textItems[i + j].text;
}, true)
textContent.forEach((item, index) => { ) {
if (item.text.toLowerCase().trim() === normalizedSearch) { for (
positions.push(index); let k = textItems[i].i;
} k < textItems[i + s.split(" ").length]?.i ||
}); k < textItems[i + s.split(" ").length - 1]?.i;
k++
if (positions.length === 0) {
let cumulativeText = '';
const textBoundaries: { start: number; end: number; index: number }[] = [];
textContent.forEach((item, index) => {
const start = cumulativeText.length;
cumulativeText += item.text;
const end = cumulativeText.length;
textBoundaries.push({ start, end, index });
});
const lowerCumulative = cumulativeText.toLowerCase();
let searchIndex = lowerCumulative.indexOf(normalizedSearch);
while (searchIndex !== -1) {
const endIndex = searchIndex + normalizedSearch.length;
textBoundaries.forEach(boundary => {
if (
(boundary.start <= searchIndex && searchIndex < boundary.end) || // Search starts in this item
(boundary.start < endIndex && endIndex <= boundary.end) || // Search ends in this item
(searchIndex <= boundary.start && boundary.end <= endIndex) // This item is completely within search
) { ) {
if (!positions.includes(boundary.index)) { tmpPos.push(textContent[k].posKey);
positions.push(boundary.index);
}
} }
}); }
searchIndex = lowerCumulative.indexOf(normalizedSearch, searchIndex + 1);
} }
} }
return positions.sort((a, b) => a - b);
}; if (focusHighlight?.page === pageNumber) {
highlight if (focusHighlight.text.split(" ")[0] === e.text) {
.filter(h => h.page === pageNumber) if (
.forEach(highlightItem => { focusHighlight.text.split(" ").reduce((prev, curr, j) => {
const positions = findTextPositions(highlightItem.text); return prev && curr === textItems[i + j].text;
positions.forEach(pos => { }, true)
if (pos >= 0 && pos < textContent.length) { ) {
tmpPos.push(textContent[pos].posKey); for (
let k = textItems[i].i;
k < textItems[i + focusHighlight.text.split(" ").length]?.i ||
k < textItems[i + focusHighlight.text.split(" ").length - 1]?.i;
k++
) {
tmpPosHighlight.push(textContent[k].posKey);
}
} }
});
});
if (focusHighlight?.page === pageNumber && focusHighlight.text) {
const positions = findTextPositions(focusHighlight.text);
positions.forEach(pos => {
if (pos >= 0 && pos < textContent.length) {
tmpPosHighlight.push(textContent[pos].posKey);
} }
}); }
} });
setPosHighlight(tmpPos);
setPosHighlight([...new Set(tmpPos)]); setPosHighlightFocus(tmpPosHighlight);
setPosHighlightFocus([...new Set(tmpPosHighlight)]);
}, [highlight, focusHighlight, pageNumber, textContent]); }, [highlight, focusHighlight, pageNumber, textContent]);
const onGetTextSuccess: OnGetTextSuccess = useCallback((fullText) => { const onGetTextSuccess: OnGetTextSuccess = useCallback((fullText) => {

View File

@ -1,5 +1,4 @@
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import EditIcon from "@mui/icons-material/Edit";
import { import {
Box, Box,
Button, Button,
@ -27,7 +26,7 @@ import {
useSuspenseQuery, useSuspenseQuery,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect, useState, type KeyboardEvent } from "react"; import { useEffect, useState } from "react";
import PDFViewer from "../components/pdfViewer"; import PDFViewer from "../components/pdfViewer";
import { fetchPutKPI } from "../util/api"; import { fetchPutKPI } from "../util/api";
import { kpiQueryOptions } from "../util/query"; import { kpiQueryOptions } from "../util/query";
@ -66,73 +65,25 @@ function ExtractedResultsPage() {
const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
const [customValue, setCustomValue] = useState(""); const [customValue, setCustomValue] = useState("");
const [customPage, setCustomPage] = useState("");
const [editingCustomPage, setEditingCustomPage] = useState(false);
const [focusHighlightOverride, setFocusHighlightOverride] = useState<{ page: number; text: string } | null>(null);
const originalValue = kpiValues[0]?.entity || ""; const originalValue = kpiValues[0]?.entity || "";
const originalPage = kpiValues[0]?.page || 0; const selectedValue =
selectedIndex === -1 ? customValue : kpiValues[selectedIndex]?.entity || "";
// Funktion, um gleiche Werte zusammenzufassen und die Seiten zu sammeln
function groupKpiValues(values: Array<{ entity: string; page: number; [key: string]: any }>): Array<{ entity: string; pages: number[]; [key: string]: any }> {
const map = new Map<string, { entity: string; pages: number[]; [key: string]: any }>();
values.forEach((item: { entity: string; page: number; [key: string]: any }) => {
const key = item.entity.toLowerCase();
if (!map.has(key)) {
map.set(key, { ...item, pages: [item.page] });
} else {
const existingEntry = map.get(key)!;
if (!existingEntry.pages.includes(item.page)) {
existingEntry.pages.push(item.page);
}
}
});
return Array.from(map.values());
}
const groupedKpiValues: Array<{ entity: string; pages: number[]; [key: string]: any }> = groupKpiValues(kpiValues);
const selectedValue: string =
selectedIndex === -1 ? customValue : groupedKpiValues[selectedIndex]?.entity || "";
const selectedPage =
selectedIndex === -1
? (parseInt(customPage) > 0 ? parseInt(customPage) : 1)
: groupedKpiValues[selectedIndex]?.pages[0] || 1;
// Um zu prüfen, ob der Wert nur aus Leerzeichen besteht
const isSelectedValueEmpty = selectedIndex === -1 ? customValue.trim() === "" : !selectedValue;
const focusHighlight = focusHighlightOverride || {
page: groupedKpiValues.at(selectedIndex)?.pages[0] || -1,
text: groupedKpiValues.at(selectedIndex)?.entity || "",
};
useEffect(() => { useEffect(() => {
const valueChanged = selectedValue !== originalValue; setHasChanges(selectedValue !== originalValue);
const pageChanged = selectedPage !== originalPage; }, [selectedValue, originalValue]);
setHasChanges(valueChanged || pageChanged);
}, [selectedValue, selectedPage, originalValue, originalPage]);
const { mutate: updateKPI } = useMutation({ const { mutate: updateKPI } = useMutation({
mutationFn: () => { mutationFn: () => {
const updatedData = { ...kpiData }; const updatedData = { ...kpiData };
let baseObject; let baseObject;
if (selectedIndex >= 0) { if (selectedIndex >= 0) {
// Das Originalobjekt mit allen Feldern für diesen Wert suchen baseObject = kpiValues[selectedIndex];
const original = kpiValues.find(v => v.entity.toLowerCase() === groupedKpiValues[selectedIndex].entity.toLowerCase()) as { status?: string; source?: string } | undefined;
baseObject = {
label: kpi.toUpperCase(),
entity: groupedKpiValues[selectedIndex].entity,
page: groupedKpiValues[selectedIndex].pages[0],
status: original?.status || "single-source",
source: original?.source || "auto",
};
} else { } else {
baseObject = { baseObject = {
label: kpi.toUpperCase(), label: kpi.toUpperCase(),
entity: selectedValue, entity: selectedValue,
page: selectedPage, page: 0,
status: "single-source", status: "single-source",
source: "manual", source: "manual",
}; };
@ -141,7 +92,6 @@ function ExtractedResultsPage() {
{ {
...baseObject, ...baseObject,
entity: selectedValue, entity: selectedValue,
page: selectedPage,
}, },
]; ];
return fetchPutKPI(Number(pitchBook), updatedData); return fetchPutKPI(Number(pitchBook), updatedData);
@ -165,14 +115,11 @@ function ExtractedResultsPage() {
const value = event.target.value; const value = event.target.value;
if (value === "custom") { if (value === "custom") {
setSelectedIndex(-1); setSelectedIndex(-1);
setFocusHighlightOverride(null);
} else { } else {
const index = Number.parseInt(value); const index = Number.parseInt(value);
setSelectedIndex(index); setSelectedIndex(index);
setCurrentPage(groupedKpiValues[index].pages[0]); setCurrentPage(kpiValues[index].page);
setCustomValue(""); setCustomValue("");
setCustomPage("");
setFocusHighlightOverride(null);
} }
}; };
@ -182,33 +129,12 @@ function ExtractedResultsPage() {
const value = event.target.value; const value = event.target.value;
setCustomValue(value); setCustomValue(value);
setSelectedIndex(-1); setSelectedIndex(-1);
setFocusHighlightOverride(null);
}
const handleCustomPageChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const value = event.target.value;
// Allow empty string or positive numbers only (no 0)
if (value === '' || (/^\d+$/.test(value) && parseInt(value) > 0)) {
setCustomPage(value);
}
}; };
const handleRowClick = (index: number) => { const handleRowClick = (index: number) => {
setCurrentPage(groupedKpiValues[index].pages[0]); setCurrentPage(kpiValues[index].page);
setSelectedIndex(index); setSelectedIndex(index);
setCustomValue(""); setCustomValue("");
setCustomPage("");
setFocusHighlightOverride(null);
};
const handlePageClick = (page: number, entity: string) => {
setCurrentPage(page);
setFocusHighlightOverride({
page: page,
text: entity,
});
}; };
const handleBackClick = () => { const handleBackClick = () => {
@ -240,16 +166,6 @@ function ExtractedResultsPage() {
updateKPI(); updateKPI();
}; };
const startCustomPageEditing = () => {
setEditingCustomPage(true);
};
const handleCustomPageKeyPress = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" || e.key === "Escape") {
setEditingCustomPage(false);
}
};
return ( return (
<Box p={4}> <Box p={4}>
<Box sx={{ display: "flex", alignItems: "center", mb: 3 }}> <Box sx={{ display: "flex", alignItems: "center", mb: 3 }}>
@ -287,18 +203,18 @@ function ExtractedResultsPage() {
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell width="85%"> <TableCell>
<strong>Gefundene Werte</strong> <strong>Gefundene Werte</strong>
</TableCell> </TableCell>
<TableCell align="center" width="15%"> <TableCell align="center">
<strong>Seiten</strong> <strong>Seite</strong>
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{groupedKpiValues.map((item, index) => ( {kpiValues.map((item, index) => (
<TableRow <TableRow
key={`${item.entity}_${item.pages.join('_')}_${index}`} key={`${item.entity}_${item.page}_${index}`}
sx={{ sx={{
"&:hover": { backgroundColor: "#f9f9f9" }, "&:hover": { backgroundColor: "#f9f9f9" },
cursor: "pointer", cursor: "pointer",
@ -337,19 +253,16 @@ function ExtractedResultsPage() {
</Box> </Box>
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
{item.pages.map((page: number, i: number) => ( <Link
<Link component="button"
key={page} onClick={(e: React.MouseEvent) => {
component="button" e.stopPropagation();
onClick={(e: React.MouseEvent) => { setCurrentPage(item.page);
e.stopPropagation(); }}
handlePageClick(page, item.entity); sx={{ cursor: "pointer" }}
}} >
sx={{ cursor: "pointer", ml: i > 0 ? 1 : 0 }} {item.page}
> </Link>
{page}
</Link>
))}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
@ -369,7 +282,6 @@ function ExtractedResultsPage() {
}} }}
onClick={() => { onClick={() => {
setSelectedIndex(-1); setSelectedIndex(-1);
setFocusHighlightOverride(null);
}} }}
> >
<Radio <Radio
@ -386,84 +298,25 @@ function ExtractedResultsPage() {
}, },
}} }}
/> />
<Box sx={{ width: '100%' }}>
<TextField
placeholder="Einen abweichenden Wert eingeben..."
value={customValue}
onChange={handleCustomValueChange}
variant="standard"
fullWidth
InputProps={{
disableUnderline: true,
}}
sx={{
"& .MuiInput-input": {
padding: 0,
},
}}
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
}}
error={selectedIndex === -1 && customValue !== "" && customValue.trim() === ""}
helperText={selectedIndex === -1 && customValue !== "" && customValue.trim() === "" ? "Der Wert, der angegeben wurde, ist leer." : ""}
/>
</Box>
</Box>
</TableCell>
<TableCell align="center">
{editingCustomPage ? (
<TextField <TextField
value={customPage} placeholder="Einen abweichenden Wert eingeben..."
onChange={handleCustomPageChange} value={customValue}
onKeyDown={handleCustomPageKeyPress} onChange={handleCustomValueChange}
onBlur={() => setEditingCustomPage(false)}
autoFocus
size="small"
variant="standard" variant="standard"
fullWidth
InputProps={{
disableUnderline: true,
}}
sx={{ sx={{
width: "60px",
"& .MuiInput-input": { "& .MuiInput-input": {
textAlign: "center" padding: 0,
} },
}}
inputProps={{
min: 0,
style: { textAlign: 'center' }
}}
/>
) : (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 1,
cursor: "pointer",
minHeight: "24px",
minWidth: "100px",
margin: "0 auto",
}} }}
onClick={(e: React.MouseEvent) => { onClick={(e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
startCustomPageEditing();
}} }}
> />
<span>{customPage || "..."}</span> </Box>
<EditIcon
fontSize="small"
sx={{
color: "black",
opacity: 0.7,
transition: "opacity 0.2s ease",
ml: 1
}}
onClick={(e) => {
e.stopPropagation();
startCustomPageEditing();
}}
/>
</Box>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableBody> </TableBody>
@ -495,17 +348,20 @@ function ExtractedResultsPage() {
pitchBookId={pitchBook} pitchBookId={pitchBook}
currentPage={currentPage} currentPage={currentPage}
onPageChange={setCurrentPage} onPageChange={setCurrentPage}
highlight={groupedKpiValues highlight={Object.values(kpiValues)
.map((k) => k.pages.map((page: number) => ({ page, text: k.entity }))) .flat()
.reduce((acc, val) => acc.concat(val), [])} .map((k) => ({ page: k.page, text: k.entity }))}
focusHighlight={focusHighlight} focusHighlight={{
page: kpiValues.at(selectedIndex)?.page || -1,
text: kpiValues.at(selectedIndex)?.entity || "",
}}
/> />
</Paper> </Paper>
<Box mt={2} display="flex" justifyContent="flex-end" gap={2}> <Box mt={2} display="flex" justifyContent="flex-end" gap={2}>
<Button <Button
variant="contained" variant="contained"
onClick={handleAcceptReview} onClick={handleAcceptReview}
disabled={isSelectedValueEmpty} disabled={!selectedValue}
sx={{ sx={{
backgroundColor: "#383838", backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" }, "&:hover": { backgroundColor: "#2e2e2e" },

View File

@ -8,8 +8,6 @@ export interface Kennzahl {
example: string; example: string;
position: number; position: number;
active: boolean; active: boolean;
exampleText?: string;
markedValue?: string;
} }
export const typeDisplayMapping: Record<string, string> = { export const typeDisplayMapping: Record<string, string> = {

View File

@ -1,6 +1,6 @@
import type { Kennzahl } from "@/types/kpi"; import type { Kennzahl } from "@/types/kpi";
const API_HOST = import.meta.env.VITE_API_HOST || "http://localhost:5050"; const API_HOST = import.meta.env.VITE_API_HOST || 'http://localhost:5050';
export { API_HOST }; export { API_HOST };
@ -15,7 +15,9 @@ export const fetchKPI = async (
source: string; source: string;
}[]; }[];
}> => { }> => {
const response = await fetch(`${API_HOST}/api/pitch_book/${pitchBookId}`); const response = await fetch(
`${API_HOST}/api/pitch_book/${pitchBookId}`,
);
const data = await response.json(); const data = await response.json();
return data.kpi ? getKPI(data.kpi) : {}; return data.kpi ? getKPI(data.kpi) : {};
@ -44,10 +46,13 @@ export const fetchPutKPI = async (
const formData = new FormData(); const formData = new FormData();
formData.append("kpi", JSON.stringify(flattenKPIArray(kpi))); formData.append("kpi", JSON.stringify(flattenKPIArray(kpi)));
const response = await fetch(`${API_HOST}/api/pitch_book/${pitchBookId}`, { const response = await fetch(
method: "PUT", `${API_HOST}/api/pitch_book/${pitchBookId}`,
body: formData, {
}); method: "PUT",
body: formData,
},
);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@ -114,11 +119,3 @@ export async function fetchPitchBooks() {
} }
return response.json(); return response.json();
} }
export async function fetchPitchBooksById(id: number) {
const response = await fetch(`${API_HOST}/api/pitch_book/${id}`);
if (!response.ok) {
throw new Error("Failed to fetch pitch books");
}
return response.json();
}

View File

@ -1,11 +0,0 @@
export const formatDate = (dateString: string): string => {
const date = new Date(dateString);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-based
const day = String(date.getDate()).padStart(2, '0');
const year = date.getFullYear();
return `${hours}:${minutes} ${day}.${month}.${year}`;
};