"""Tätigkeitsnachweis-Generator – Streamlit App."""
import sys
from pathlib import Path
import streamlit as st
# Projekt-Root zum Python-Path hinzufügen
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:
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(
'
'
''
'❌ Validierung fehlgeschlagen
',
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(
''
''
'⚠️ Hinweise
',
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'{an}'
for an in antragsnummern
)
st.markdown(f"**Antragsnummern:**
{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 .xlsx-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
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(
''
''
'❌ Validierung fehlgeschlagen
',
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_target_cells", 0)
total = tpl_result.info.get("total_target_cells", 0)
st.success(f"✅ {found}/{total} Zielfelder gefunden")
if tpl_result.warnings:
st.markdown(
''
''
'⚠️ Hinweise
',
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
st.markdown(
'',
unsafe_allow_html=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, value=st.session_state[key], key=key):
selected_employees.append(emp)
st.markdown("
", unsafe_allow_html=True)
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.")
# --- 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,
)
st.success(f"✅ {len(generated)} Tätigkeitsnachweise erstellt:")
for fpath in generated:
st.text(f"📄 {fpath.name}")
except Exception as e:
st.error(f"Fehler bei der Generierung: {e}")