Compare commits

...

33 Commits

Author SHA1 Message Date
Abdulrahman Dabbagh c9de2cb027 Frontend wie besprochen erweitert, Flask-Backend um Training ergänzt, neue spaCy-Trainingsdaten (.json) und Trainings-Skript hinzugefügt 2025-06-25 16:36:49 +02:00
Abdulrahman Dabbagh 2f1d591202 Merge remote-tracking branch 'origin/main' into neue-Kennzahl-spacy 2025-06-25 16:20:43 +02:00
Abdulrahman Dabbagh 4ca8314ed2 Merge pull request '#86-87-90' (#92) from #86-87-90 into main
Reviewed-on: #92
2025-06-24 23:04:32 +02:00
s8613 ed34687dc4 Fix highlighting for hyphenated values. Improve highlighting text matching logic. Handle text split across items. 2025-06-24 12:49:47 +02:00
s8613 82f02c0772 Fixed page highlighting in multiple kpis details page. 2025-06-24 11:40:04 +02:00
s8613 685edc9ad2 Added favicon, webtitle and Deka logo. 2025-06-24 11:24:02 +02:00
Abdulrahman Dabbagh 15d4eb1207 Merge pull request '#82-Seiten-Edit-icon-weg,-nur-inline-edit' (#84) from #82-Seiten-Edit-icon-weg,-nur-inline-edit into main
Reviewed-on: #84
2025-06-22 19:33:52 +02:00
Zainab MohamedBasheer 38d24555ff Merge pull request 'Add date to PitchBooksTable and reorder table' (#83) from #81-reorder-table into main
Reviewed-on: #83
2025-06-22 16:06:56 +02:00
Zainab MohamedBasheer af2161eea7 Merge branch 'main' into #81-reorder-table 2025-06-22 16:04:41 +02:00
Anastasia Hanna Ougolnikova 609ec5284a Merge pull request 'Fixed Bug Ticket #63 and #71' (#80) from #71-enable-empty-values into main
Reviewed-on: #80
2025-06-22 14:09:10 +02:00
Zainab2604 73deb3912e Fix hopefully last merge conflict 2025-06-22 13:56:11 +02:00
Zainab2604 8b430f693d Fix merge commit 3 2025-06-22 13:45:20 +02:00
Zainab2604 64426a5f83 Fix merge commit 2 2025-06-22 13:43:56 +02:00
Zainab2604 f6747e45ac Fix merge conflict 2025-06-22 13:41:45 +02:00
Zainab2604 61dcc76203 Eine Seite kommt nur einmal in der Liste vor 2025-06-22 12:44:30 +02:00
s8613 6ddda5036d Changed icon color. 2025-06-21 12:47:42 +02:00
s8613 c023959a97 Removed icon and hover effect. Added inline editing. Changed icon color. 2025-06-21 12:46:39 +02:00
Jaronim Pracht c593fc0e47 Add date to PitchBooksTable and reorder table
Add new date formatting utility and update PitchBooksTable to display
the upload date for each pitch book. The sorting order
for pitch books has been reversed to show the most recent uploads first.
2025-06-20 18:50:08 +02:00
Zainab MohamedBasheer f70df881de Merge pull request '#76-Hinzufügen-einer-Seitenanzahl-bei-neuen-Kennzahlen' (#78) from #76-Hinzufügen-einer-Seitenanzahl-bei-neuen-Kennzahlen into main
Reviewed-on: #78
2025-06-20 10:32:25 +02:00
Abdulrahman Dabbagh abccb43741 WIP: Fehler F401 behoben, cleanup vor Branchwechsel 2025-06-20 10:16:36 +02:00
s8613 e3149e0aa4 Merge remote-tracking branch 'origin/main' into #76-Hinzufügen-einer-Seitenanzahl-bei-neuen-Kennzahlen
# Conflicts:
#	project/frontend/src/components/KennzahlenTable.tsx
2025-06-20 06:35:00 +02:00
Abdulrahman Dabbagh 443a0c5d66 Merge pull request '#77-progress-pitch-books-table' (#79) from #77-progress-pitch-books-table into main
Reviewed-on: #79
2025-06-20 01:43:07 +02:00
Zainab2604 60b303d92e Fixed Bug Ticket #63 and #71 2025-06-19 22:27:52 +02:00
Jaronim Pracht 9a08331574 Merge branch 'main' into #77-progress-pitch-books-table 2025-06-18 16:40:14 +02:00
Jaronim Pracht 017670f95e Add progress on pitch-books table 2025-06-18 16:38:11 +02:00
s8613 f205165350 Fixed table columns sizing always changing widths 2025-06-17 13:05:13 +02:00
s8613 eea2d015b2 fixed small styling issue 2025-06-17 12:22:17 +02:00
s8613 6816e1a2d7 fixed small styling issue 2025-06-17 12:12:33 +02:00
s8613 26224671bb Fixed change of tabs 2025-06-17 11:45:54 +02:00
s8613 615007b437 Fixed change of tabs 2025-06-17 11:45:30 +02:00
s8613 ac8cf2f7c2 Updated extractedResult_.$pitchBook.$kpi.tsx for page number editing 2025-06-17 11:43:20 +02:00
s8613 c144db3f13 Updated KennzahlenTabel 2025-06-17 11:29:49 +02:00
Jaronim Pracht 6ef258c999 Merge branch 'main' into #23-Progress 2025-06-17 10:51:21 +02:00
46 changed files with 12908 additions and 452 deletions

1652
annotation_data.json 100644

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,61 +0,0 @@
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",
"example",
"position",
"active"
"active",
]
for field in required_fields:
if field not in data:
@ -61,7 +61,7 @@ def create_kpi_setting():
translation=data["translation"],
example=data["example"],
position=data["position"],
active=data["active"]
active=data["active"],
)
db.session.add(new_kpi_setting)
@ -136,7 +136,12 @@ def update_kpi_positions():
try:
for update_item in data:
if "id" not in update_item or "position" not in update_item:
return jsonify({"error": "Each item must have 'id' and 'position' fields"}), 400
return (
jsonify(
{"error": "Each item must have 'id' and 'position' fields"}
),
400,
)
kpi_setting = KPISettingModel.query.get_or_404(update_item["id"])
kpi_setting.position = update_item["position"]
@ -148,4 +153,4 @@ def update_kpi_positions():
except Exception as e:
db.session.rollback()
return jsonify({"error": f"Failed to update positions: {str(e)}"}), 500
return jsonify({"error": f"Failed to update positions: {str(e)}"}), 500

View File

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

View File

@ -1,10 +1,11 @@
from flask import Blueprint, request, jsonify, send_file
from io import BytesIO
from model.spacy_model import SpacyModel
import puremagic
from werkzeug.utils import secure_filename
from model.database import db
import os
import json
spacy_controller = Blueprint("spacy", __name__, url_prefix="/api/spacy")
@ -91,3 +92,39 @@ def delete_file(id):
db.session.commit()
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,4 +14,5 @@ def init_db(app):
with app.app_context():
db.create_all()
from model.seed_data import seed_default_kpi_settings
seed_default_kpi_settings()

View File

@ -1,26 +0,0 @@
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,10 +38,12 @@ class KPISettingModel(db.Model):
"translation": self.translation,
"example": self.example,
"position": self.position,
"active": self.active
"active": self.active,
}
def __init__(self, name, description, mandatory, type, translation, example, position, active):
def __init__(
self, name, description, mandatory, type, translation, example, position, active
):
self.name = name
self.description = description
self.mandatory = mandatory

View File

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

View File

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

View File

@ -3,12 +3,19 @@ from extractSpacy import extract
import requests
import os
import json
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
VALIDATE_SERVICE_URL = os.getenv("VALIDATE_SERVICE_URL", "http://localhost:5054/validate")
@app.route('/extract', methods=['POST'])
VALIDATE_SERVICE_URL = os.getenv(
"VALIDATE_SERVICE_URL", "http://localhost:5054/validate"
)
@app.route("/extract", methods=["POST"])
def extract_pdf():
json_data = request.get_json()
@ -16,19 +23,19 @@ def extract_pdf():
pages_data = json_data["extracted_text_per_page"]
entities_json = extract(pages_data)
entities = json.loads(entities_json) if isinstance(entities_json, str) else entities_json
entities = (
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] Payload: {validate_payload} entities for pitchbook {pitchbook_id}")
try:
response = requests.post(VALIDATE_SERVICE_URL, json=validate_payload, timeout=600)
response = requests.post(
VALIDATE_SERVICE_URL, json=validate_payload, timeout=600
)
print(f"[SPACY] Validate service response: {response.status_code}")
if response.status_code != 200:
print(f"[SPACY] Validate service error: {response.text}")
@ -38,5 +45,40 @@ def extract_pdf():
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__":
app.run(host="0.0.0.0", port=5052, debug=True)
app.run(host="0.0.0.0", port=5052, debug=True)

View File

@ -0,0 +1,35 @@
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

@ -0,0 +1,130 @@
[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

@ -0,0 +1,63 @@
{
"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

@ -0,0 +1,13 @@
{
"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

@ -0,0 +1 @@
¥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

@ -0,0 +1,3 @@
{
"mode":"default"
}

View File

@ -0,0 +1,122 @@
[
{
"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,6 +37,8 @@ services:
retries: 10
ports:
- 5050:5000
volumes:
- ./backend/spacy-service/spacy_training:/app/spacy_training
ocr:
build:

View File

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

View File

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

6335
project/frontend/package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -3,6 +3,7 @@ import { Box, Typography, Button, Paper, TextField, FormControlLabel,
import { useState, useEffect } from "react";
import type { Kennzahl } from "../types/kpi";
import { typeDisplayMapping } from "../types/kpi";
// import { saveAs } from "file-saver";
interface KPIFormProps {
mode: 'add' | 'edit';
@ -19,7 +20,9 @@ const emptyKPI: Partial<Kennzahl> = {
type: 'string',
translation: '',
example: '',
active: true
active: true,
exampleText: '',
markedValue: '',
};
export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }: KPIFormProps) {
@ -40,16 +43,60 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
return;
}
if (!formData.exampleText?.trim()) {
alert('Beispielsatz ist erforderlich');
return;
}
if (!formData.markedValue?.trim()) {
alert('Bezeichneter Wert im Satz ist erforderlich');
return;
}
setIsSaving(true);
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);
} catch (error) {
console.error('Error saving KPI:', error);
alert("SpaCy-Eintrag erfolgreich gespeichert!");
} catch (e: any) {
alert(e.message || "Fehler beim Erzeugen des Trainingsbeispiels.");
console.error(e);
} finally {
setIsSaving(false);
}
};
const handleCancel = () => {
onCancel();
};
@ -106,18 +153,40 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
<Box mb={4}>
<Typography variant="h6" fontWeight="bold" mb={2}>
Beschreibung
Beispielsatz
</Typography>
<TextField
fullWidth
multiline
rows={3}
label="Beschreibung"
value={formData.description || ''}
onChange={(e) => updateField('description', e.target.value)}
helperText="Beschreibung der Kennzahl"
label="Beispielsatz"
required
value={formData.exampleText || ''}
onChange={(e) => updateField('exampleText', e.target.value)}
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}>
<FormControlLabel
control={
@ -244,4 +313,28 @@ export function KPIForm({ mode, initialData, onSave, onCancel, loading = false }
</Box>
</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,25 +42,43 @@ export default function KennzahlenTable({
data,
pdfId,
settings,
from
from,
}: KennzahlenTableProps) {
const [editingIndex, setEditingIndex] = useState<string>("");
const [editValue, setEditValue] = useState("");
const [editingPageIndex, setEditingPageIndex] = useState<string>("");
const [editPageValue, setEditPageValue] = useState("");
const [hoveredPageIndex, setHoveredPageIndex] = useState<string>("");
const navigate = useNavigate();
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: (id: string) => {
mutationFn: (params: { id: string; newValue?: string; newPage?: number }) => {
const { id, newValue, newPage } = params;
const key = id.toUpperCase();
const updatedData = { ...data };
updatedData[key] = data[key]?.map((item) => ({
...item,
entity: editValue,
})) || [{ label: key, entity: editValue }];
if (data[key] && data[key].length > 0) {
updatedData[key] = data[key].map((item) => ({
...item,
...(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);
},
onMutate: async (id: string) => {
onMutate: async (params: { id: string; newValue?: string; newPage?: number }) => {
const { id, newValue, newPage } = params;
await queryClient.cancelQueries({
queryKey: ["pitchBookKPI", pdfId],
});
@ -71,10 +89,23 @@ export default function KennzahlenTable({
queryClient.setQueryData(["pitchBookKPI", pdfId], () => {
const updatedData = { ...data };
updatedData[key] = data[key]?.map((item) => ({
...item,
entity: editValue,
})) || [{ label: key, entity: editValue }];
if (data[key] && data[key].length > 0) {
updatedData[key] = data[key].map((item) => ({
...item,
...(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;
});
@ -99,19 +130,41 @@ export default function KennzahlenTable({
setEditValue(value);
};
const startPageEditing = (value: number, index: string) => {
setEditingPageIndex(index);
setEditPageValue(value.toString());
};
// Bearbeitung beenden und Wert speichern
const handleSave = async (index: string) => {
// await updateKennzahl(rows[index].label, editValue);
mutate(index);
mutate({ id: index, newValue: editValue });
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
const handleKeyPress = (e: KeyboardEvent<HTMLDivElement>, index: string) => {
if (e.key === "Enter") {
handleSave(index);
} else if (e.key === "Escape") {
setEditingIndex("null");
setEditingIndex("");
}
};
const handlePageKeyPress = (e: KeyboardEvent<HTMLDivElement>, index: string) => {
if (e.key === "Enter") {
handlePageSave(index);
} else if (e.key === "Escape") {
setEditingPageIndex("");
}
};
@ -131,14 +184,16 @@ export default function KennzahlenTable({
<Table>
<TableHead>
<TableRow>
<TableCell>
<TableCell width="30%">
<strong>Kennzahl</strong>
</TableCell>
<TableCell>
<TableCell width="55%">
<strong>Wert</strong>
</TableCell>
<TableCell align="center">
<strong>Seite</strong>
<TableCell align="center" width="15%">
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 1 }}>
<strong>Seite</strong>
</Box>
</TableCell>
</TableRow>
</TableHead>
@ -165,9 +220,17 @@ export default function KennzahlenTable({
borderColor = "#f6ed48";
}
const currentPage = row.extractedValues.at(0)?.page ?? 0;
const isPageHovered = hoveredPageIndex === row.setting.name;
const canEditPage = !hasMultipleValues;
return (
<TableRow key={row.setting.name}>
<TableCell>{row.setting.name}</TableCell>
<TableCell>{row.setting.name}
{row.setting.mandatory && (
<span> *</span>
)}
</TableCell>
<TableCell
onClick={() => {
// Only allow inline editing for non-multiple value cells
@ -229,12 +292,17 @@ export default function KennzahlenTable({
</Tooltip>
) : (
<Tooltip
title={hasNoValue ?
<>
<b>Problem</b>
<br />
Es wurden keine Kennzahlen gefunden. Bitte ergänzen!
</> : ""
title={
hasNoValue ? (
<>
<b>Problem</b>
<br />
Es wurden keine Kennzahlen gefunden. Bitte
ergänzen!
</>
) : (
""
)
}
placement="bottom"
arrow
@ -261,7 +329,10 @@ export default function KennzahlenTable({
}}
>
{hasNoValue && (
<ErrorOutlineIcon fontSize="small" color="error" />
<ErrorOutlineIcon
fontSize="small"
color="error"
/>
)}
{editingIndex === row.setting.name ? (
<TextField
@ -300,21 +371,99 @@ export default function KennzahlenTable({
)}
</TableCell>
<TableCell align="center">
{(row.extractedValues.at(0)?.page ?? 0) > 0 ? (
<Link
component="button"
onClick={() => {
const extractedValue = row.extractedValues.at(0);
if (extractedValue?.page && extractedValue.page > 0) {
onPageClick?.(Number(extractedValue.page), extractedValue.entity || "");
{editingPageIndex === row.setting.name ? (
<TextField
value={editPageValue}
onChange={(e) => {
const value = e.target.value;
if (value === '' || /^\d+$/.test(value)) {
setEditPageValue(value);
}
}}
sx={{ cursor: "pointer" }}
>
{row.extractedValues.at(0)?.page}
</Link>
onKeyDown={(e) => handlePageKeyPress(e, row.setting.name)}
onBlur={() => handlePageSave(row.setting.name)}
autoFocus
size="small"
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>
</TableRow>
@ -324,4 +473,4 @@ export default function KennzahlenTable({
</Table>
</TableContainer>
);
}
}

View File

@ -1,186 +1,402 @@
import { Box, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography, CircularProgress, Chip } from "@mui/material";
import { useSuspenseQuery } from "@tanstack/react-query";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import HourglassEmptyIcon from "@mui/icons-material/HourglassEmpty";
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 { useCallback, useEffect, useState } from "react";
import { socket } from "../socket";
import { fetchPitchBooksById } from "../util/api";
import { pitchBooksQueryOptions } from "../util/query";
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
import { formatDate } from "../util/date"
interface PitchBook {
id: number;
filename: string;
created_at: string;
kpi?: string | {
[key: string]: {
label: string;
entity: string;
page: number;
status: string;
source: string;
}[];
};
status?: 'processing' | 'completed';
id: number;
filename: string;
created_at: string;
kpi?:
| string
| {
[key: string]: {
label: string;
entity: string;
page: number;
status: string;
source: string;
}[];
};
status?: "processing" | "completed";
}
export function PitchBooksTable() {
const navigate = useNavigate();
const { data: pitchBooks, isLoading } = useSuspenseQuery(pitchBooksQueryOptions());
const [loadingPitchBooks, setLoadingPitchBooks] = useState<
{
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) => {
navigate({
to: "/extractedResult/$pitchBook",
params: { pitchBook: pitchBookId.toString() },
search: { from: "overview" }
});
};
const handleRowClick = (pitchBookId: number) => {
navigate({
to: "/extractedResult/$pitchBook",
params: { pitchBook: pitchBookId.toString() },
search: { from: "overview" },
});
};
const getKPIValue = (pitchBook: PitchBook, fieldName: string): string => {
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 onConnection = useCallback(() => {
console.log("connected");
}, []);
return kpiObj[fieldName]?.[0]?.entity || 'N/A';
} catch {
return 'N/A';
}
}
const queryClient = useQueryClient();
return (pitchBook.kpi as any)[fieldName]?.[0]?.entity || 'N/A';
};
const onProgress = useCallback(
(progress: { id: number; progress: number }) => {
if (progress.progress === 100) {
setLoadingPitchBooks((prev) => {
const intervalId = prev.find(
(item) => item.id === progress.id,
)?.intervalId;
intervalId && clearInterval(intervalId);
const getStatus = (pitchBook: PitchBook) => {
if (pitchBook.kpi &&
((typeof pitchBook.kpi === 'string' && pitchBook.kpi !== '{}') ||
(typeof pitchBook.kpi === 'object' && Object.keys(pitchBook.kpi).length > 0))) {
return 'completed';
}
return 'processing';
};
return [...prev.filter((item) => item.id !== progress.id)];
});
queryClient.invalidateQueries({
queryKey: pitchBooksQueryOptions().queryKey,
});
} else {
setLoadingPitchBooks((prev) => {
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;
if (isLoading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" height="400px">
<CircularProgress sx={{ color: "#383838" }} />
</Box>
);
}
return [
...prev.filter((e) => e.id !== progress.id),
{
id: progress.id,
progress: oldItem?.progress ?? progress.progress,
filename: oldItem?.filename,
buffer: oldItem ? oldItem.buffer + 0.5 : 0,
intervalId: oldItem.intervalId,
created_at: oldItem?.created_at,
},
];
});
}, 400);
return (
<TableContainer
component={Paper}
sx={{
width: "85%",
maxWidth: 1200,
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
}}
>
<Table>
<TableHead>
<TableRow sx={{ backgroundColor: "#f5f5f5" }}>
<TableCell sx={{ width: "60px" }}></TableCell>
<TableCell sx={{ fontWeight: "bold" }}>Fondsname</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>Fondsmanager</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>Dateiname</TableCell>
<TableCell sx={{ fontWeight: "bold", width: "120px" }}>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{pitchBooks.map((pitchBook: PitchBook) => {
const status = getStatus(pitchBook);
const fundName = getKPIValue(pitchBook, 'FONDSNAME') ||
getKPIValue(pitchBook, 'FUND_NAME') ||
getKPIValue(pitchBook, 'NAME');
fetchPitchBooksById(progress.id)
.then((res) => {
setLoadingPitchBooks((prev) => [
...prev.filter((item) => item.id !== progress.id),
{
id: progress.id,
progress: progress.progress,
filename: res.filename,
buffer: 0,
intervalId,
created_at: res.created_at,
},
]);
})
.catch((err) => {
console.error(err);
});
}
return [
...prev.filter((item) => item.id !== progress.id),
{
id: progress.id,
progress: progress.progress,
filename: oldItem?.filename,
created_at: oldItem?.created_at,
buffer: 0,
intervalId,
},
];
});
}
},
[queryClient],
);
const manager = getKPIValue(pitchBook, 'FONDSMANAGER') ||
getKPIValue(pitchBook, 'MANAGER') ||
getKPIValue(pitchBook, 'PORTFOLIO_MANAGER');
useEffect(() => {
socket.on("connect", onConnection);
socket.on("progress", onProgress);
return () => {
socket.off("connect", onConnection);
socket.off("progress", onProgress);
};
}, [onConnection, onProgress]);
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>
<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>
);
}
const getKPIValue = (pitchBook: PitchBook, fieldName: string): string => {
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, item) => {
if (!acc[item.label]) acc[item.label] = [];
acc[item.label].push(item);
return acc;
}, {})
: parsedKPI;
return kpiObj[fieldName]?.[0]?.entity || "N/A";
} catch {
return "N/A";
}
}
return pitchBook.kpi[fieldName]?.[0]?.entity || "N/A";
};
const getStatus = (pitchBook: PitchBook) => {
if (
pitchBook.kpi &&
((typeof pitchBook.kpi === "string" && pitchBook.kpi !== "{}") ||
(typeof pitchBook.kpi === "object" &&
Object.keys(pitchBook.kpi).length > 0))
) {
return "completed";
}
return "processing";
};
if (isLoading) {
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="400px"
>
<CircularProgress sx={{ color: "#383838" }} />
</Box>
);
}
return (
<TableContainer
component={Paper}
sx={{
width: "85%",
maxWidth: 1200,
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
}}
>
<Table>
<TableHead>
<TableRow sx={{ backgroundColor: "#f5f5f5" }}>
<TableCell sx={{ width: "60px" }} />
<TableCell sx={{ fontWeight: "bold" }}>Fondsname</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>Fondsmanager</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>Hochgeladen am</TableCell>
<TableCell sx={{ fontWeight: "bold" }}>Dateiname</TableCell>
<TableCell sx={{ fontWeight: "bold", width: "120px" }}>
Status
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{loadingPitchBooks
.sort((a, b) => a.id - b.id)
.map((pitchBook) => (
<TableRow key={pitchBook.id}>
<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 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,11 +1,12 @@
import SettingsIcon from "@mui/icons-material/Settings";
import { Backdrop, Box, Button, IconButton, Paper } from "@mui/material";
import { useNavigate } from "@tanstack/react-router";
import { Backdrop, Box, Button, IconButton, Paper, Typography } from "@mui/material";
import { useNavigate, useRouter } from "@tanstack/react-router";
import { useCallback, useEffect, useState } from "react";
import FileUpload from "react-material-file-upload";
import { socket } from "../socket";
import { CircularProgressWithLabel } from "./CircularProgressWithLabel";
import { API_HOST } from "../util/api";
import { CircularProgressWithLabel } from "./CircularProgressWithLabel";
import DekaLogo from "../assets/Deka_logo.png";
export default function UploadPage() {
const [files, setFiles] = useState<File[]>([]);
@ -13,6 +14,7 @@ export default function UploadPage() {
const [loadingState, setLoadingState] = useState<number | null>(null);
const fileTypes = ["pdf"];
const navigate = useNavigate();
const router = useRouter();
const uploadFile = useCallback(async () => {
const formData = new FormData();
@ -86,26 +88,50 @@ export default function UploadPage() {
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
justifyContent="flex-start"
height="100vh"
bgcolor="white"
pt={3}
>
<Box
width="100%"
maxWidth="1300px"
display="flex"
justifyContent="flex-end"
px={2}
justifyContent="space-between"
alignItems="center"
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" })}>
<SettingsIcon fontSize="large" />
</IconButton>
</Box>
<Typography
variant="h4"
component="h1"
sx={{
fontWeight: "bold",
color: "#383838",
marginBottom: 12,
marginTop: 6,
}}
>
Pitchbook Extractor
</Typography>
<Paper
elevation={3}
sx={{
width: 900,
height: 500,
width: 800,
height: 400,
backgroundColor: "#eeeeee",
borderRadius: 4,
display: "flex",
@ -178,6 +204,7 @@ export default function UploadPage() {
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },
}}
onMouseEnter={() => router.preloadRoute({ to: "/pitchbooks" })}
onClick={() => navigate({ to: "/pitchbooks" })}
>
Alle Pitch Books anzeigen
@ -185,4 +212,4 @@ export default function UploadPage() {
</Box>
</>
);
}
}

View File

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

View File

@ -1,4 +1,5 @@
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import EditIcon from "@mui/icons-material/Edit";
import {
Box,
Button,
@ -26,7 +27,7 @@ import {
useSuspenseQuery,
} from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { useEffect, useState, type KeyboardEvent } from "react";
import PDFViewer from "../components/pdfViewer";
import { fetchPutKPI } from "../util/api";
import { kpiQueryOptions } from "../util/query";
@ -65,25 +66,73 @@ function ExtractedResultsPage() {
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
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 selectedValue =
selectedIndex === -1 ? customValue : kpiValues[selectedIndex]?.entity || "";
const originalPage = kpiValues[0]?.page || 0;
// 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(() => {
setHasChanges(selectedValue !== originalValue);
}, [selectedValue, originalValue]);
const valueChanged = selectedValue !== originalValue;
const pageChanged = selectedPage !== originalPage;
setHasChanges(valueChanged || pageChanged);
}, [selectedValue, selectedPage, originalValue, originalPage]);
const { mutate: updateKPI } = useMutation({
mutationFn: () => {
const updatedData = { ...kpiData };
let baseObject;
if (selectedIndex >= 0) {
baseObject = kpiValues[selectedIndex];
// Das Originalobjekt mit allen Feldern für diesen Wert suchen
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 {
baseObject = {
label: kpi.toUpperCase(),
entity: selectedValue,
page: 0,
page: selectedPage,
status: "single-source",
source: "manual",
};
@ -92,6 +141,7 @@ function ExtractedResultsPage() {
{
...baseObject,
entity: selectedValue,
page: selectedPage,
},
];
return fetchPutKPI(Number(pitchBook), updatedData);
@ -115,11 +165,14 @@ function ExtractedResultsPage() {
const value = event.target.value;
if (value === "custom") {
setSelectedIndex(-1);
setFocusHighlightOverride(null);
} else {
const index = Number.parseInt(value);
setSelectedIndex(index);
setCurrentPage(kpiValues[index].page);
setCurrentPage(groupedKpiValues[index].pages[0]);
setCustomValue("");
setCustomPage("");
setFocusHighlightOverride(null);
}
};
@ -129,12 +182,33 @@ function ExtractedResultsPage() {
const value = event.target.value;
setCustomValue(value);
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) => {
setCurrentPage(kpiValues[index].page);
setCurrentPage(groupedKpiValues[index].pages[0]);
setSelectedIndex(index);
setCustomValue("");
setCustomPage("");
setFocusHighlightOverride(null);
};
const handlePageClick = (page: number, entity: string) => {
setCurrentPage(page);
setFocusHighlightOverride({
page: page,
text: entity,
});
};
const handleBackClick = () => {
@ -166,6 +240,16 @@ function ExtractedResultsPage() {
updateKPI();
};
const startCustomPageEditing = () => {
setEditingCustomPage(true);
};
const handleCustomPageKeyPress = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" || e.key === "Escape") {
setEditingCustomPage(false);
}
};
return (
<Box p={4}>
<Box sx={{ display: "flex", alignItems: "center", mb: 3 }}>
@ -203,18 +287,18 @@ function ExtractedResultsPage() {
<Table>
<TableHead>
<TableRow>
<TableCell>
<TableCell width="85%">
<strong>Gefundene Werte</strong>
</TableCell>
<TableCell align="center">
<strong>Seite</strong>
<TableCell align="center" width="15%">
<strong>Seiten</strong>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{kpiValues.map((item, index) => (
{groupedKpiValues.map((item, index) => (
<TableRow
key={`${item.entity}_${item.page}_${index}`}
key={`${item.entity}_${item.pages.join('_')}_${index}`}
sx={{
"&:hover": { backgroundColor: "#f9f9f9" },
cursor: "pointer",
@ -253,16 +337,19 @@ function ExtractedResultsPage() {
</Box>
</TableCell>
<TableCell align="center">
<Link
component="button"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
setCurrentPage(item.page);
}}
sx={{ cursor: "pointer" }}
>
{item.page}
</Link>
{item.pages.map((page: number, i: number) => (
<Link
key={page}
component="button"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
handlePageClick(page, item.entity);
}}
sx={{ cursor: "pointer", ml: i > 0 ? 1 : 0 }}
>
{page}
</Link>
))}
</TableCell>
</TableRow>
))}
@ -282,6 +369,7 @@ function ExtractedResultsPage() {
}}
onClick={() => {
setSelectedIndex(-1);
setFocusHighlightOverride(null);
}}
>
<Radio
@ -298,25 +386,84 @@ 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
placeholder="Einen abweichenden Wert eingeben..."
value={customValue}
onChange={handleCustomValueChange}
value={customPage}
onChange={handleCustomPageChange}
onKeyDown={handleCustomPageKeyPress}
onBlur={() => setEditingCustomPage(false)}
autoFocus
size="small"
variant="standard"
fullWidth
InputProps={{
disableUnderline: true,
}}
sx={{
width: "60px",
"& .MuiInput-input": {
padding: 0,
},
textAlign: "center"
}
}}
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) => {
e.stopPropagation();
startCustomPageEditing();
}}
/>
</Box>
>
<span>{customPage || "..."}</span>
<EditIcon
fontSize="small"
sx={{
color: "black",
opacity: 0.7,
transition: "opacity 0.2s ease",
ml: 1
}}
onClick={(e) => {
e.stopPropagation();
startCustomPageEditing();
}}
/>
</Box>
)}
</TableCell>
</TableRow>
</TableBody>
@ -348,20 +495,17 @@ function ExtractedResultsPage() {
pitchBookId={pitchBook}
currentPage={currentPage}
onPageChange={setCurrentPage}
highlight={Object.values(kpiValues)
.flat()
.map((k) => ({ page: k.page, text: k.entity }))}
focusHighlight={{
page: kpiValues.at(selectedIndex)?.page || -1,
text: kpiValues.at(selectedIndex)?.entity || "",
}}
highlight={groupedKpiValues
.map((k) => k.pages.map((page: number) => ({ page, text: k.entity })))
.reduce((acc, val) => acc.concat(val), [])}
focusHighlight={focusHighlight}
/>
</Paper>
<Box mt={2} display="flex" justifyContent="flex-end" gap={2}>
<Button
variant="contained"
onClick={handleAcceptReview}
disabled={!selectedValue}
disabled={isSelectedValueEmpty}
sx={{
backgroundColor: "#383838",
"&:hover": { backgroundColor: "#2e2e2e" },

View File

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

View File

@ -1,6 +1,6 @@
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 };
@ -15,9 +15,7 @@ export const fetchKPI = async (
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();
return data.kpi ? getKPI(data.kpi) : {};
@ -46,13 +44,10 @@ export const fetchPutKPI = async (
const formData = new FormData();
formData.append("kpi", JSON.stringify(flattenKPIArray(kpi)));
const response = await fetch(
`${API_HOST}/api/pitch_book/${pitchBookId}`,
{
method: "PUT",
body: formData,
},
);
const response = await fetch(`${API_HOST}/api/pitch_book/${pitchBookId}`, {
method: "PUT",
body: formData,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@ -119,3 +114,11 @@ export async function fetchPitchBooks() {
}
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

@ -0,0 +1,11 @@
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}`;
};