diff --git a/0_application/CHANGELOG.md b/0_application/CHANGELOG.md index d9e4687..788ad2a 100644 --- a/0_application/CHANGELOG.md +++ b/0_application/CHANGELOG.md @@ -9,6 +9,12 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). ## [1.3.2] – 2026-10-07 +### Hinzugefügt + +- **Input-Dateien hinzufügen**: Bei leerem Input-Ordner können `.xlsx`-Dateien per Drag-and-drop oder Dateiauswahl in den Input-Ordner kopiert werden +- **Input-Ordner öffnen**: Ein Button öffnet den Input-Ordner direkt im Explorer +- **Input-Ordner neu einlesen**: Dateiliste kann über Button aktualisiert werden + ### Behoben - EXE erkennt laufende Entwicklungsinstanz und fragt vor dem Neustart diff --git a/0_application/README.md b/0_application/README.md index f49c2ea..6396bbe 100644 --- a/0_application/README.md +++ b/0_application/README.md @@ -9,6 +9,10 @@ Um das Programm zu starten: Doppelklick auf `Tätigkeitsnachweis-Generator.exe` ## Bedienung 1. **Input-Datei wählen** – Dropdown zeigt alle `.xlsx`-Dateien aus dem `input/`-Ordner + - Bei leerem Input-Ordner: „Input-Ordner öffnen“ öffnet den Explorer. + - Excel-Dateien können per Drag-and-drop hochgeladen werden. Sie werden in den + Input-Ordner kopiert; danach wird die Dateiliste automatisch neu eingelesen. + - Nach manuellem Kopieren im Explorer „Input-Ordner neu einlesen“ anklicken. - Nach Auswahl wird die Datei sofort validiert - Angezeigt werden: Anzahl Mitarbeiter, Antragsnummern, Einträge pro Monat 2. **Template wählen** – Dropdown zeigt alle `.xlsx`-Dateien aus dem `template/`-Ordner @@ -54,4 +58,4 @@ Mapping von Input-Feldern auf Template-Zellen: - Pro selektiertem Mitarbeiter wird eine `.xlsx`-Datei erstellt - Jeder Monat wird als eigenes Sheet (Kopie des Templates) eingefügt -- Dateiname: `{Nachname}_{Vorname}_Tätigkeitsnachweis.xlsx` \ No newline at end of file +- Dateiname: `{Nachname}_{Vorname}_Tätigkeitsnachweis.xlsx` diff --git a/0_application/app.py b/0_application/app.py index da2d889..4a6fb5a 100644 --- a/0_application/app.py +++ b/0_application/app.py @@ -32,6 +32,14 @@ from src.writer import generate_activity_reports from version_check import UPDATE_DIR, get_latest_version +def open_input_folder(directory: Path): + directory.mkdir(parents=True, exist_ok=True) + if sys.platform == "win32": + os.startfile(str(directory)) + else: + subprocess.run(["open" if sys.platform == "darwin" else "xdg-open", str(directory)], check=True) + + def set_active_employee(employee_name: str): """Merkt die zuletzt angeklickte Person für die Monatsliste.""" st.session_state.active_month_employee = employee_name @@ -155,9 +163,64 @@ col1, col2 = st.columns(2) with col1: st.subheader("1. Input-Datei") input_files = get_available_input_files(app_config) + for message in st.session_state.pop("input_upload_messages", []): + st.success(message) + for message in st.session_state.pop("input_upload_errors", []): + st.error(message) if not input_files: - st.warning("Keine .xlsx-Dateien im input/-Ordner gefunden.") + with st.container(border=True, key="input_file_panel"): + st.markdown("**Excel-Datei hinzufügen**") + st.caption("Keine .xlsx-Dateien im Input/-Ordner gefunden.") + input_directory = Path(app_config.input_dir) + uploaded_files = st.file_uploader( + "Excel-Dateien hierher ziehen oder auswählen", + type=["xlsx"], accept_multiple_files=True, + key=f"input_upload_{st.session_state.get('input_upload_generation', 0)}", + label_visibility="collapsed", + ) + if uploaded_files: + saved, errors = [], [] + for uploaded_file in uploaded_files: + try: + name = uploaded_file.name + if (Path(name).name != name or "/" in name or "\\" in name + or Path(name).suffix.lower() != ".xlsx" or name.startswith("~")): + raise ValueError("Bitte eine .xlsx-Datei mit gültigem Dateinamen auswählen.") + input_directory.mkdir(parents=True, exist_ok=True) + destination = input_directory / name + counter = 1 + while True: + try: + with destination.open("xb") as output: + output.write(uploaded_file.getbuffer()) + break + except FileExistsError: + counter += 1 + destination = input_directory / f"{Path(name).stem} ({counter}).xlsx" + saved.append(f"{destination.name} wurde in den Input-Ordner kopiert.") + except (OSError, ValueError) as exc: + errors.append(f"{uploaded_file.name} konnte nicht übernommen werden: {exc}") + st.session_state.input_upload_messages = saved + st.session_state.input_upload_errors = errors + st.session_state.input_upload_generation = st.session_state.get("input_upload_generation", 0) + 1 + st.session_state.last_input_file = None + st.rerun() + st.caption("Dateien hierher ziehen oder auswählen. Sie werden automatisch im Input-Ordner gespeichert.") + folder_action, refresh_action = st.columns(2) + with folder_action: + if st.button("Ordner öffnen", icon="📁", key="open_input_directory", use_container_width=True): + try: + open_input_folder(input_directory) + except (OSError, subprocess.SubprocessError) as exc: + st.error(f"Der Input-Ordner konnte nicht geöffnet werden: {exc}") + with refresh_action: + if st.button("Neu einlesen", icon="🔄", key="refresh_input_directory", use_container_width=True, + help="Dateiliste aktualisieren, nachdem Dateien im Explorer hinzugefügt wurden."): + st.rerun() selected_input = None + st.session_state.parsed_records = [] + st.session_state.input_validated = False + st.session_state.last_input_file = None else: # Bei nur einer Datei: automatisch selektieren (keine Leerauswahl) if len(input_files) == 1: diff --git a/1_build-pipeline/.last_version b/1_build-pipeline/.last_version index d5e98f7..785cda8 100644 --- a/1_build-pipeline/.last_version +++ b/1_build-pipeline/.last_version @@ -1 +1 @@ -1.3.2 \ No newline at end of file +1.3.3 \ No newline at end of file