add build pipline
This commit is contained in:
1 parent
94e245a3a0
commit
c9dac7b9b8
5 files changed
+680
-1
No files matched your search
@@ -0,0 +1,330 @@
|
||||
"""Tätigkeitsnachweis-Generator – Streamlit App."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import streamlit as st
|
||||
|
||||
# Projekt-Root zum Python-Path hinzufügen
|
||||
# Im gebundelten Modus (PyInstaller) zeigt APP_BASE_DIR auf das Verzeichnis der .exe,
|
||||
# wo config/, input/, template/, output/ liegen. Im Entwicklungsmodus bleibt es None
|
||||
# und PROJECT_ROOT wird aus __file__ abgeleitet.
|
||||
_BASE_DIR = os.environ.get("APP_BASE_DIR")
|
||||
if _BASE_DIR:
|
||||
PROJECT_ROOT = Path(_BASE_DIR).resolve()
|
||||
else:
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from src.config import (
|
||||
load_settings,
|
||||
load_input_schema,
|
||||
load_output_mapping,
|
||||
get_available_input_files,
|
||||
get_available_templates,
|
||||
)
|
||||
from src.models import AppConfig
|
||||
from src.parser import parse_input_file, group_by_employee, get_unique_antragsnummern, get_unique_employees
|
||||
from src.validator import validate_input_file, validate_template
|
||||
from src.writer import generate_activity_reports
|
||||
|
||||
# --- Seite konfigurieren ---
|
||||
st.set_page_config(
|
||||
page_title="Tätigkeitsnachweis-Generator",
|
||||
page_icon="📋",
|
||||
layout="wide",
|
||||
)
|
||||
|
||||
st.title("📋 Tätigkeitsnachweis-Generator")
|
||||
st.markdown("Erstellt Tätigkeitsnachweise aus Input-Daten und einem Excel-Template.")
|
||||
|
||||
# --- Konfiguration laden ---
|
||||
CONFIG_DIR = PROJECT_ROOT / "config"
|
||||
|
||||
@st.cache_data
|
||||
def load_app_config():
|
||||
config = load_settings(CONFIG_DIR, PROJECT_ROOT)
|
||||
config.input_schema = load_input_schema(CONFIG_DIR)
|
||||
config.output_mapping = load_output_mapping(CONFIG_DIR)
|
||||
return config
|
||||
|
||||
try:
|
||||
app_config = load_app_config()
|
||||
except Exception as e:
|
||||
st.error(f"Fehler beim Laden der Konfiguration: {e}")
|
||||
st.stop()
|
||||
|
||||
# --- Session State initialisieren ---
|
||||
if "parsed_records" not in st.session_state:
|
||||
st.session_state.parsed_records = []
|
||||
if "input_validated" not in st.session_state:
|
||||
st.session_state.input_validated = False
|
||||
if "template_validated" not in st.session_state:
|
||||
st.session_state.template_validated = False
|
||||
# Caches für Validierung/Parsing – nur bei Dateiwechsel neu ausführen
|
||||
if "last_input_file" not in st.session_state:
|
||||
st.session_state.last_input_file = None
|
||||
if "last_template_file" not in st.session_state:
|
||||
st.session_state.last_template_file = None
|
||||
if "cached_val_result" not in st.session_state:
|
||||
st.session_state.cached_val_result = None
|
||||
if "cached_tpl_result" not in st.session_state:
|
||||
st.session_state.cached_tpl_result = None
|
||||
|
||||
# --- Layout: zwei Spalten ---
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.subheader("1. Input-Datei")
|
||||
input_files = get_available_input_files(app_config)
|
||||
if not input_files:
|
||||
st.warning("Keine .xlsx-Dateien im input/-Ordner gefunden.")
|
||||
selected_input = None
|
||||
else:
|
||||
# Bei nur einer Datei: automatisch selektieren (keine Leerauswahl)
|
||||
if len(input_files) == 1:
|
||||
selected_input = st.selectbox(
|
||||
"Input-Datei wählen:",
|
||||
options=input_files,
|
||||
)
|
||||
else:
|
||||
selected_input = st.selectbox(
|
||||
"Input-Datei wählen:",
|
||||
options=[""] + input_files,
|
||||
format_func=lambda x: "– Bitte wählen –" if x == "" else x,
|
||||
)
|
||||
|
||||
if selected_input:
|
||||
input_path = Path(app_config.input_dir) / selected_input
|
||||
|
||||
# Nur validieren/parsen wenn sich die Datei geändert hat
|
||||
if selected_input != st.session_state.last_input_file:
|
||||
with st.spinner("Validiere Input-Datei…"):
|
||||
val_result = validate_input_file(input_path, app_config.input_schema, app_config.month_sheet_pattern)
|
||||
|
||||
if val_result.is_valid:
|
||||
records = parse_input_file(
|
||||
input_path,
|
||||
app_config.input_schema,
|
||||
app_config.month_sheet_pattern,
|
||||
app_config.skip_empty_name,
|
||||
app_config.skip_empty_antragsnummer,
|
||||
)
|
||||
st.session_state.parsed_records = records
|
||||
st.session_state.input_validated = True
|
||||
else:
|
||||
st.session_state.parsed_records = []
|
||||
st.session_state.input_validated = False
|
||||
|
||||
st.session_state.cached_val_result = val_result
|
||||
st.session_state.last_input_file = selected_input
|
||||
else:
|
||||
val_result = st.session_state.cached_val_result
|
||||
|
||||
if not val_result.is_valid:
|
||||
st.markdown(
|
||||
'<div style="border:2px solid #e74c3c; border-radius:8px; padding:12px; '
|
||||
'background:#fdedec; margin-bottom:12px">'
|
||||
'<span style="color:#b03a2e; font-weight:bold; font-size:1.1em">'
|
||||
'❌ Validierung fehlgeschlagen</span></div>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
for err in val_result.errors:
|
||||
st.error(f"🔴 {err}")
|
||||
st.session_state.input_validated = False
|
||||
else:
|
||||
if val_result.warnings:
|
||||
st.markdown(
|
||||
'<div style="border:2px solid #f39c12; border-radius:8px; padding:8px 12px; '
|
||||
'background:#fef9e7; margin-bottom:8px">'
|
||||
'<span style="color:#7d6608; font-weight:bold">'
|
||||
'⚠️ Hinweise</span></div>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
for warn in val_result.warnings:
|
||||
st.warning(warn)
|
||||
|
||||
# Zeige Zusammenfassung aus gecachten Records
|
||||
records = st.session_state.parsed_records
|
||||
employees = get_unique_employees(records)
|
||||
antragsnummern = get_unique_antragsnummern(records)
|
||||
|
||||
st.success(f"✅ {len(records)} Einträge in {val_result.info.get('sheets_with_data', 0)} Monats-Sheets")
|
||||
pills = " ".join(
|
||||
f'<span style="display:inline-block; background:#e8f0fe; color:#1a73e8; '
|
||||
f'border-radius:16px; padding:4px 12px; margin:3px; font-size:0.9em; '
|
||||
f'white-space:nowrap">{an}</span>'
|
||||
for an in antragsnummern
|
||||
)
|
||||
st.markdown(f"**Antragsnummern:**<br>{pills}", unsafe_allow_html=True)
|
||||
|
||||
with col2:
|
||||
st.subheader("2. Template")
|
||||
template_files = get_available_templates(app_config)
|
||||
if not template_files:
|
||||
st.warning("Keine .pdf-Dateien im template/-Ordner gefunden.")
|
||||
selected_template = None
|
||||
else:
|
||||
# Lock-Files herausfiltern
|
||||
real_templates = [t for t in template_files if not t.startswith("~")]
|
||||
if not real_templates:
|
||||
real_templates = template_files
|
||||
# Bei nur einem Template: automatisch selektieren (keine Leerauswahl)
|
||||
if len(real_templates) == 1:
|
||||
selected_template = st.selectbox(
|
||||
"Template wählen:",
|
||||
options=real_templates,
|
||||
)
|
||||
else:
|
||||
selected_template = st.selectbox(
|
||||
"Template wählen:",
|
||||
options=[""] + real_templates,
|
||||
format_func=lambda x: "– Bitte wählen –" if x == "" else x,
|
||||
)
|
||||
|
||||
if selected_template:
|
||||
# Nur validieren wenn sich das Template geändert hat
|
||||
if selected_template != st.session_state.last_template_file:
|
||||
template_path = Path(app_config.template_dir) / selected_template
|
||||
with st.spinner("Validiere Template…"):
|
||||
tpl_result = validate_template(template_path, app_config.output_mapping)
|
||||
st.session_state.cached_tpl_result = tpl_result
|
||||
st.session_state.last_template_file = selected_template
|
||||
st.session_state.template_validated = tpl_result.is_valid
|
||||
else:
|
||||
tpl_result = st.session_state.cached_tpl_result
|
||||
|
||||
if not tpl_result.is_valid:
|
||||
st.markdown(
|
||||
'<div style="border:2px solid #e74c3c; border-radius:8px; padding:12px; '
|
||||
'background:#fdedec; margin-bottom:12px">'
|
||||
'<span style="color:#b03a2e; font-weight:bold; font-size:1.1em">'
|
||||
'❌ Validierung fehlgeschlagen</span></div>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
for err in tpl_result.errors:
|
||||
st.error(f"🔴 {err}")
|
||||
st.session_state.template_validated = False
|
||||
else:
|
||||
st.session_state.template_validated = True
|
||||
found = tpl_result.info.get("found_fields", 0)
|
||||
total = tpl_result.info.get("total_fields", 0)
|
||||
st.success(f"✅ {found}/{total} Platzhalter-Felder gefunden")
|
||||
if tpl_result.warnings:
|
||||
st.markdown(
|
||||
'<div style="border:2px solid #f39c12; border-radius:8px; padding:8px 12px; '
|
||||
'background:#fef9e7; margin-bottom:8px">'
|
||||
'<span style="color:#7d6608; font-weight:bold">'
|
||||
'⚠️ Hinweise</span></div>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
for warn in tpl_result.warnings:
|
||||
st.warning(warn)
|
||||
|
||||
# --- Mitarbeiter-Auswahl ---
|
||||
st.subheader("3. Mitarbeiter auswählen")
|
||||
|
||||
if st.session_state.input_validated and st.session_state.parsed_records:
|
||||
employees = get_unique_employees(st.session_state.parsed_records)
|
||||
st.caption(f"{len(employees)} Mitarbeiter gefunden – wähle aus, für wen Tätigkeitsnachweise erstellt werden sollen:")
|
||||
|
||||
# "Alle" / "Keine" Buttons
|
||||
btn_col1, btn_col2 = st.columns(2)
|
||||
if btn_col1.button("☑ Alle auswählen", use_container_width=True):
|
||||
for emp in employees:
|
||||
st.session_state[f"sel_{emp}"] = True
|
||||
if btn_col2.button("☐ Keine auswählen", use_container_width=True):
|
||||
for emp in employees:
|
||||
st.session_state[f"sel_{emp}"] = False
|
||||
|
||||
# Mitarbeiter-Liste mit Rahmen
|
||||
with st.container(border=True):
|
||||
selected_employees = []
|
||||
for emp in employees:
|
||||
key = f"sel_{emp}"
|
||||
if key not in st.session_state:
|
||||
st.session_state[key] = True
|
||||
if st.checkbox(emp, key=key):
|
||||
selected_employees.append(emp)
|
||||
|
||||
st.caption(f"{len(selected_employees)} von {len(employees)} Mitarbeitern ausgewählt")
|
||||
else:
|
||||
selected_employees = []
|
||||
if not st.session_state.input_validated:
|
||||
st.info("Bitte zuerst eine gültige Input-Datei auswählen.")
|
||||
|
||||
# --- Hilfsfunktion zum Öffnen von Dateien/Ordnern ---
|
||||
def open_path(path_str: str):
|
||||
"""Öffnet eine Datei mit dem Standardprogramm oder einen Ordner im Dateimanager.
|
||||
Funktioniert plattformübergreifend: macOS, Windows, Linux."""
|
||||
if sys.platform == "win32":
|
||||
os.startfile(path_str)
|
||||
elif sys.platform == "darwin":
|
||||
subprocess.run(["open", path_str])
|
||||
else:
|
||||
subprocess.run(["xdg-open", path_str])
|
||||
|
||||
|
||||
# --- Generate Button ---
|
||||
st.divider()
|
||||
gen_col1, gen_col2, gen_col3 = st.columns([1, 1, 2])
|
||||
with gen_col1:
|
||||
generate_clicked = st.button(
|
||||
"🚀 Tätigkeitsnachweise generieren",
|
||||
type="primary",
|
||||
disabled=not (st.session_state.input_validated and st.session_state.template_validated and selected_employees),
|
||||
)
|
||||
|
||||
if generate_clicked:
|
||||
if not selected_employees:
|
||||
st.error("Keine Mitarbeiter ausgewählt.")
|
||||
else:
|
||||
# Filtere Records auf selektierte Mitarbeiter
|
||||
all_groups = group_by_employee(st.session_state.parsed_records)
|
||||
selected_groups = {name: all_groups[name] for name in selected_employees if name in all_groups}
|
||||
|
||||
template_path = Path(app_config.template_dir) / selected_template
|
||||
output_dir = Path(app_config.output_dir)
|
||||
|
||||
with st.spinner(f"Generiere {len(selected_groups)} Tätigkeitsnachweise…"):
|
||||
try:
|
||||
generated = generate_activity_reports(
|
||||
employee_records=selected_groups,
|
||||
template_path=template_path,
|
||||
mapping=app_config.output_mapping,
|
||||
output_dir=output_dir,
|
||||
filename_pattern=app_config.filename_pattern,
|
||||
fontsize=app_config.fontsize,
|
||||
)
|
||||
st.session_state.generated_files = generated
|
||||
except Exception as e:
|
||||
st.error(f"Fehler bei der Generierung: {e}")
|
||||
|
||||
# --- Generierte Dateien anzeigen (aus Session State, übersteht Reruns) ---
|
||||
if "generated_files" in st.session_state and st.session_state.generated_files:
|
||||
generated = st.session_state.generated_files
|
||||
st.success(f"✅ {len(generated)} Tätigkeitsnachweise erstellt:")
|
||||
for i, fpath in enumerate(generated):
|
||||
col_file, col_open, col_folder = st.columns([0.78, 0.11, 0.11])
|
||||
with col_file:
|
||||
st.markdown(f"📄 `{fpath.name}`")
|
||||
with col_open:
|
||||
st.button(
|
||||
"🔗 Öffnen",
|
||||
key=f"open_file_{i}",
|
||||
on_click=open_path,
|
||||
args=(str(fpath),),
|
||||
help="Datei mit Standardprogramm öffnen",
|
||||
use_container_width=True,
|
||||
)
|
||||
with col_folder:
|
||||
st.button(
|
||||
"📂",
|
||||
key=f"open_folder_{i}",
|
||||
on_click=open_path,
|
||||
args=(str(fpath.parent),),
|
||||
help="Ordner im Finder öffnen",
|
||||
use_container_width=True,
|
||||
)
|
||||
Reference in new issue
Block a user