Files
sufw-taetigkeitsnachweise/app.py
T

225 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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
# --- 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
# Validiere und parse sofort
with st.spinner("Validiere Input-Datei…"):
val_result = validate_input_file(input_path, app_config.input_schema, app_config.month_sheet_pattern)
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)
# Parse die Datei
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
# Zeige Zusammenfassung
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")
st.metric("Mitarbeiter", len(employees))
st.metric("Antragsnummern", len(antragsnummern))
with st.expander("Antragsnummern anzeigen"):
for an in antragsnummern:
st.text(f"• {an}")
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:
template_path = Path(app_config.template_dir) / selected_template
with st.spinner("Validiere Template…"):
tpl_result = validate_template(template_path, app_config.output_mapping)
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_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(
'<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)
selected_employees = st.multiselect(
f"{len(employees)} Mitarbeiter gefunden – wähle aus, für wen Tätigkeitsnachweise erstellt werden sollen:",
options=employees,
default=employees,
)
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}")