Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34730276d7 | ||
|
|
c12b15e021 | ||
|
|
d027bedcb8 | ||
|
|
eb1dd45e57 | ||
|
|
6f1d807f3c |
No files matched your search
@@ -7,6 +7,31 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.3] – 2026-08-11
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
|
||||||
|
- **Monatsauswahl pro Mitarbeiter**: Tätigkeitsnachweise können nun für individuell gewählte Monate erzeugt werden.
|
||||||
|
- Die Monatsauswahl erscheint rechts neben der Mitarbeiterliste für die zuletzt angeklickte Person.
|
||||||
|
- Die Auswahl wird in Kurzform direkt hinter dem Namen angezeigt, z. B. `(Jan, Mär, Dez)`.
|
||||||
|
- Standardmäßig sind die letzten zwei vorhandenen Monate ausgewählt.
|
||||||
|
- Schaltflächen zum Auswählen bzw. Abwählen aller Monate ergänzen die Monatsliste.
|
||||||
|
|
||||||
|
- **Versionsprüfung**: Die App prüft beim Start im zentralen SMB-Verzeichnis auf neuere
|
||||||
|
`Tätigkeitsnachweis-Generator vX.Y.Z.zip`- und künftig auch `.exe`-Dateien.
|
||||||
|
|
||||||
|
- **Versionsanzeige**: Die aktuell installierte Programmversion wird dezent am unteren rechten Rand der App angezeigt.
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
|
||||||
|
- **Mitarbeiterauswahl**: Mitarbeiter sind nach dem Einlesen einer Input-Datei nicht mehr standardmäßig ausgewählt.
|
||||||
|
Die Buttons zum Auswählen bzw. Abwählen aller Mitarbeiter befinden sich direkt über der Mitarbeiterliste.
|
||||||
|
|
||||||
|
- **Update-Hinweis**: Bei einer neueren zentralen Version wird ein schwebender Hinweis oben rechts angezeigt,
|
||||||
|
einschließlich Schließen- und Ordner-Schaltfläche.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.2.1] – 2026-06-30
|
## [1.2.1] – 2026-06-30
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
|
||||||
|
# Tätigkeitsnachweis-Generator
|
||||||
|
|
||||||
|
Streamlit-App zur automatisierten Erstellung von Tätigkeitsnachweisen aus Excel-Daten.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. **Python 3.9+ installieren** (falls nicht vorhanden): https://python.org
|
||||||
|
2. **Virtuelle Umgebung erstellen und Abhängigkeiten installieren:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
.venv\Scripts\activate # Windows
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **App starten:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
streamlit run app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder einfach `start.bat` (Windows) bzw. `./start.sh` (macOS/Linux) ausführen.
|
||||||
|
|
||||||
|
## Cold Run – Headless-Generierung
|
||||||
|
|
||||||
|
`coldrun.py` erzeugt ohne GUI einen Tätigkeitsnachweis für die erste Person –
|
||||||
|
automatisch mit der ersten Input- und Template-Datei. Ergebnis wird auf macOS
|
||||||
|
im Finder geöffnet.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Kommandozeile
|
||||||
|
.venv/bin/python coldrun.py
|
||||||
|
|
||||||
|
# oder per Doppelklick (macOS)
|
||||||
|
coldrun.command
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bedienung
|
||||||
|
|
||||||
|
1. **Input-Datei wählen** – Dropdown zeigt alle `.xlsx`-Dateien aus dem `input/`-Ordner
|
||||||
|
- 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
|
||||||
|
- Nach Auswahl wird das Template sofort validiert
|
||||||
|
- Angezeigt wird: Anzahl gefundener Zielfelder
|
||||||
|
3. **Mitarbeiter auswählen** – Multiselect-Liste aller gefundenen Mitarbeiter
|
||||||
|
- Standardmäßig sind alle ausgewählt
|
||||||
|
- Nicht selektierte Mitarbeiter werden bei der Generierung übersprungen
|
||||||
|
4. **"Tätigkeitsnachweise generieren"** klicken
|
||||||
|
- Die generierten Dateien werden im `output/`-Ordner abgelegt
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
Alle Einstellungen liegen im `config/`-Ordner:
|
||||||
|
|
||||||
|
### `settings.ini`
|
||||||
|
- `[paths]` – Ordner für Input, Template, Output
|
||||||
|
- `[output]` – Dateinamen-Pattern (`{first_name}`, `{last_name}`)
|
||||||
|
- `[parsing]` – Monats-Sheet-Erkennung, Leerzeilen-Verhalten
|
||||||
|
|
||||||
|
### `input-fields.json`
|
||||||
|
Definiert die Spalten der Input-Tabelle:
|
||||||
|
- `column`: Spaltenbuchstabe (A, B, C…)
|
||||||
|
- `field_name`: Exakte Spaltenüberschrift aus der Excel-Datei
|
||||||
|
- `type`: `string` oder `number`
|
||||||
|
- `required`: `true` oder `false`
|
||||||
|
|
||||||
|
### `output-fields.json`
|
||||||
|
Mapping von Input-Feldern auf Template-Zellen:
|
||||||
|
- `source_field`: Exakte Spaltenüberschrift aus `input-fields.json`
|
||||||
|
- `target_cell`: Zelladresse im Template (z. B. `"C16"`)
|
||||||
|
- `format`: Optionaler Format-String (z. B. `"%.2f"` für Prozentwerte)
|
||||||
|
|
||||||
|
## Erwartetes Input-Format
|
||||||
|
|
||||||
|
- Eine `.xlsx`-Datei mit einem Sheet pro Monat (z. B. "Januar 2025", "Februar 2025", …)
|
||||||
|
- Jedes Sheet hat in Zeile 1 die Spaltenüberschriften (laut `input-fields.json`)
|
||||||
|
- Daten beginnen ab Zeile 2
|
||||||
|
- Jede Zeile entspricht einer Mitarbeiter-Funktion-Kombination für diesen Monat
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
- 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`
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
End-to-End-Tests parsen die reale Input-Datei (`input/2025 JM.xlsx`) und das reale
|
||||||
|
Template (`template/Tätigkeitsnachweise Stellenförderung.xlsx`) und validieren
|
||||||
|
den gesamten Generierungs-Workflow.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test-Abhängigkeiten installieren
|
||||||
|
.venv/bin/pip install pytest
|
||||||
|
|
||||||
|
# Tests ausführen
|
||||||
|
.venv/bin/python -m pytest tests/test_e2e.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Tests prüfen:
|
||||||
|
- Vorhandensein der Input- und Template-Dateien
|
||||||
|
- Korrekte Parsierung (168 Datensätze, 19 Personen)
|
||||||
|
- Vollständige Pipeline für die erste Person („Draßdo, Cornelia"): 12 Monats-Sheets
|
||||||
|
mit allen Zellinhalten (Antragsnummer, Name, Stellenanteil, Qualifikation, Monat,
|
||||||
|
Funktion, Tätigkeit, Ort, Datum)
|
||||||
+8
-58
@@ -1,40 +1,10 @@
|
|||||||
|
|
||||||
# Tätigkeitsnachweis-Generator
|
# Tätigkeitsnachweis-Generator
|
||||||
|
|
||||||
Streamlit-App zur automatisierten Erstellung von Tätigkeitsnachweisen aus Excel-Daten.
|
Programm zur automatisierten Erstellung von Tätigkeitsnachweisen aus Excel-Daten.
|
||||||
|
Die aktuellesten Programmversion find sich immer hier:
|
||||||
|
"j:\Fachbereiche\BO\Produktionsschule\Maßnahmeunterlagen\20_Personal\05_Tätigkeitsnachweise ESF\Nachweise-automatisiert-generieren\"
|
||||||
|
|
||||||
## Setup
|
Um das Programm zu starten: Doppelklick auf `Tätigkeitsnachweis-Generator.exe`
|
||||||
|
|
||||||
1. **Python 3.9+ installieren** (falls nicht vorhanden): https://python.org
|
|
||||||
2. **Virtuelle Umgebung erstellen und Abhängigkeiten installieren:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m venv .venv
|
|
||||||
.venv\Scripts\activate # Windows
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **App starten:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
streamlit run app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Oder einfach `start.bat` (Windows) bzw. `./start.sh` (macOS/Linux) ausführen.
|
|
||||||
|
|
||||||
## Cold Run – Headless-Generierung
|
|
||||||
|
|
||||||
`coldrun.py` erzeugt ohne GUI einen Tätigkeitsnachweis für die erste Person –
|
|
||||||
automatisch mit der ersten Input- und Template-Datei. Ergebnis wird auf macOS
|
|
||||||
im Finder geöffnet.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Kommandozeile
|
|
||||||
.venv/bin/python coldrun.py
|
|
||||||
|
|
||||||
# oder per Doppelklick (macOS)
|
|
||||||
coldrun.command
|
|
||||||
```
|
|
||||||
|
|
||||||
## Bedienung
|
## Bedienung
|
||||||
|
|
||||||
@@ -45,9 +15,10 @@ coldrun.command
|
|||||||
- Nach Auswahl wird das Template sofort validiert
|
- Nach Auswahl wird das Template sofort validiert
|
||||||
- Angezeigt wird: Anzahl gefundener Zielfelder
|
- Angezeigt wird: Anzahl gefundener Zielfelder
|
||||||
3. **Mitarbeiter auswählen** – Multiselect-Liste aller gefundenen Mitarbeiter
|
3. **Mitarbeiter auswählen** – Multiselect-Liste aller gefundenen Mitarbeiter
|
||||||
- Standardmäßig sind alle ausgewählt
|
- Standardmäßig ist keiner ausgewählt
|
||||||
- Nicht selektierte Mitarbeiter werden bei der Generierung übersprungen
|
- Nicht selektierte Mitarbeiter werden bei der Generierung übersprungen
|
||||||
4. **"Tätigkeitsnachweise generieren"** klicken
|
4. **Mitarbeitermonate auswählen** – Multiselect-Liste aller verfügbaren Monate im Dokument. Vorselektiert sind die aktuell letzten beiden Monate.
|
||||||
|
5. **"Tätigkeitsnachweise generieren"** klicken
|
||||||
- Die generierten Dateien werden im `output/`-Ordner abgelegt
|
- Die generierten Dateien werden im `output/`-Ordner abgelegt
|
||||||
|
|
||||||
## Konfiguration
|
## Konfiguration
|
||||||
@@ -83,25 +54,4 @@ Mapping von Input-Feldern auf Template-Zellen:
|
|||||||
|
|
||||||
- Pro selektiertem Mitarbeiter wird eine `.xlsx`-Datei erstellt
|
- Pro selektiertem Mitarbeiter wird eine `.xlsx`-Datei erstellt
|
||||||
- Jeder Monat wird als eigenes Sheet (Kopie des Templates) eingefügt
|
- Jeder Monat wird als eigenes Sheet (Kopie des Templates) eingefügt
|
||||||
- Dateiname: `{Nachname}_{Vorname}_Tätigkeitsnachweis.xlsx`
|
- Dateiname: `{Nachname}_{Vorname}_Tätigkeitsnachweis.xlsx`
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
End-to-End-Tests parsen die reale Input-Datei (`input/2025 JM.xlsx`) und das reale
|
|
||||||
Template (`template/Tätigkeitsnachweise Stellenförderung.xlsx`) und validieren
|
|
||||||
den gesamten Generierungs-Workflow.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test-Abhängigkeiten installieren
|
|
||||||
.venv/bin/pip install pytest
|
|
||||||
|
|
||||||
# Tests ausführen
|
|
||||||
.venv/bin/python -m pytest tests/test_e2e.py -v
|
|
||||||
```
|
|
||||||
|
|
||||||
Die Tests prüfen:
|
|
||||||
- Vorhandensein der Input- und Template-Dateien
|
|
||||||
- Korrekte Parsierung (168 Datensätze, 19 Personen)
|
|
||||||
- Vollständige Pipeline für die erste Person („Draßdo, Cornelia"): 12 Monats-Sheets
|
|
||||||
mit allen Zellinhalten (Antragsnummer, Name, Stellenanteil, Qualifikation, Monat,
|
|
||||||
Funktion, Tätigkeit, Ort, Datum)
|
|
||||||
+185
-19
@@ -29,6 +29,27 @@ from src.models import AppConfig
|
|||||||
from src.parser import parse_input_file, group_by_employee, get_unique_antragsnummern, get_unique_employees
|
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.validator import validate_input_file, validate_template
|
||||||
from src.writer import generate_activity_reports
|
from src.writer import generate_activity_reports
|
||||||
|
from version_check import UPDATE_DIR, get_latest_version
|
||||||
|
|
||||||
|
|
||||||
|
def set_active_employee(employee_name: str):
|
||||||
|
"""Merkt die zuletzt angeklickte Person für die Monatsliste."""
|
||||||
|
st.session_state.active_month_employee = employee_name
|
||||||
|
|
||||||
|
|
||||||
|
def sync_employee_months(employee_name: str, months: list[str]):
|
||||||
|
"""Übernimmt Monats-Checkboxen vor dem nächsten UI-Rendern."""
|
||||||
|
st.session_state.employee_months[employee_name] = {
|
||||||
|
month for month in months
|
||||||
|
if st.session_state.get(f"month_{employee_name}_{month}", False)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def set_all_employee_months(employee_name: str, months: list[str], selected: bool):
|
||||||
|
"""Setzt alle Monats-Checkboxen einer Person gesammelt."""
|
||||||
|
st.session_state.employee_months[employee_name] = set(months) if selected else set()
|
||||||
|
for month in months:
|
||||||
|
st.session_state[f"month_{employee_name}_{month}"] = selected
|
||||||
|
|
||||||
# --- Seite konfigurieren ---
|
# --- Seite konfigurieren ---
|
||||||
st.set_page_config(
|
st.set_page_config(
|
||||||
@@ -56,6 +77,60 @@ except Exception as e:
|
|||||||
st.error(f"Fehler beim Laden der Konfiguration: {e}")
|
st.error(f"Fehler beim Laden der Konfiguration: {e}")
|
||||||
st.stop()
|
st.stop()
|
||||||
|
|
||||||
|
# --- Versionsinformation für die dezente Anzeige am Seitenende ---
|
||||||
|
current_version_text = ""
|
||||||
|
try:
|
||||||
|
local_version, latest_version = get_latest_version()
|
||||||
|
current_version_text = f"v{'.'.join(map(str, local_version))}"
|
||||||
|
if latest_version:
|
||||||
|
available_version, _ = latest_version
|
||||||
|
version_text = ".".join(map(str, available_version))
|
||||||
|
st.html(
|
||||||
|
f'''<style>
|
||||||
|
.update-notice {{
|
||||||
|
position: fixed; top: 4rem; right: 1rem; z-index: 10000;
|
||||||
|
width: 290px; padding: 8px 12px; color: #5f4700;
|
||||||
|
background: #fff8e1; border: 1px solid #e0b84c; border-radius: 8px;
|
||||||
|
box-shadow: 0 3px 12px rgba(0, 0, 0, .18); font-size: .85rem;
|
||||||
|
}}
|
||||||
|
#update-notice-dismiss {{ display:none; }}
|
||||||
|
#update-notice-dismiss:checked + .update-notice {{ display:none; }}
|
||||||
|
body:has(#update-notice-dismiss:checked) div.st-key-update_notice_open {{ display:none!important; }}
|
||||||
|
.update-notice__actions {{ display:flex; justify-content:flex-end; gap:8px; margin-top:8px; }}
|
||||||
|
.update-notice__actions--footer {{ min-height:26px; }}
|
||||||
|
.update-notice__button {{
|
||||||
|
display:inline-flex; align-items:center; justify-content:center;
|
||||||
|
width:26px; height:26px; color:#5f4700; background:#fffdf3;
|
||||||
|
border:1px solid #d9bd66; border-radius:5px; text-decoration:none;
|
||||||
|
font-size:1rem; line-height:1;
|
||||||
|
}}
|
||||||
|
.update-notice__button:hover {{ background:#f6e7b2; }}
|
||||||
|
div.st-key-update_notice_open {{
|
||||||
|
position:fixed; top:8.5rem; right:3.75rem; z-index:10001; width:26px;
|
||||||
|
}}
|
||||||
|
div.st-key-update_notice_open div[data-testid="stButton"] {{ margin:0; }}
|
||||||
|
div.st-key-update_notice_open div[data-testid="stButton"] button {{
|
||||||
|
min-width:200px; min-height:26px; height:26px; padding:0; color:#5f4700;
|
||||||
|
background:#fffdf3; border:1px solid #d9bd66; border-radius:5px;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
<input id="update-notice-dismiss" type="checkbox">
|
||||||
|
<aside class="update-notice" role="status">
|
||||||
|
<div class="update-notice__actions">
|
||||||
|
<label class="update-notice__button" for="update-notice-dismiss" title="Hinweis schließen">×</label>
|
||||||
|
</div>
|
||||||
|
<strong>Neue Version v{version_text} verfügbar</strong>
|
||||||
|
<div class="update-notice__actions update-notice__actions--footer">
|
||||||
|
</div>
|
||||||
|
</aside>'''
|
||||||
|
)
|
||||||
|
with st.container(key="update_notice_open"):
|
||||||
|
if st.button("Update Ordner öffnen 📁", key="open_update_directory", help="Update-Ordner öffnen"):
|
||||||
|
if sys.platform == "win32":
|
||||||
|
os.startfile(str(UPDATE_DIR))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# --- Session State initialisieren ---
|
# --- Session State initialisieren ---
|
||||||
if "parsed_records" not in st.session_state:
|
if "parsed_records" not in st.session_state:
|
||||||
st.session_state.parsed_records = []
|
st.session_state.parsed_records = []
|
||||||
@@ -72,7 +147,8 @@ if "cached_val_result" not in st.session_state:
|
|||||||
st.session_state.cached_val_result = None
|
st.session_state.cached_val_result = None
|
||||||
if "cached_tpl_result" not in st.session_state:
|
if "cached_tpl_result" not in st.session_state:
|
||||||
st.session_state.cached_tpl_result = None
|
st.session_state.cached_tpl_result = None
|
||||||
|
if "employee_months" not in st.session_state:
|
||||||
|
st.session_state.employee_months = {}
|
||||||
# --- Layout: zwei Spalten ---
|
# --- Layout: zwei Spalten ---
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
@@ -230,24 +306,94 @@ if st.session_state.input_validated and st.session_state.parsed_records:
|
|||||||
employees = get_unique_employees(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:")
|
st.caption(f"{len(employees)} Mitarbeiter gefunden – wähle aus, für wen Tätigkeitsnachweise erstellt werden sollen:")
|
||||||
|
|
||||||
# "Alle" / "Keine" Buttons
|
# Monatsbezeichnungen für die kompakte Anzeige hinter dem Namen
|
||||||
btn_col1, btn_col2 = st.columns(2)
|
month_labels = {
|
||||||
if btn_col1.button("☑ Alle auswählen", use_container_width=True):
|
"01": "Jan", "02": "Feb", "03": "Mär", "04": "Apr",
|
||||||
for emp in employees:
|
"05": "Mai", "06": "Jun", "07": "Jul", "08": "Aug",
|
||||||
st.session_state[f"sel_{emp}"] = True
|
"09": "Sep", "10": "Okt", "11": "Nov", "12": "Dez",
|
||||||
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
|
all_groups = group_by_employee(st.session_state.parsed_records)
|
||||||
with st.container(border=True):
|
available_months = {}
|
||||||
selected_employees = []
|
for emp in employees:
|
||||||
for emp in employees:
|
months = sorted({
|
||||||
key = f"sel_{emp}"
|
record.monat.split(".", 1)[0].zfill(2)
|
||||||
if key not in st.session_state:
|
for record in all_groups.get(emp, [])
|
||||||
st.session_state[key] = True
|
if record.monat and record.monat.split(".", 1)[0].isdigit()
|
||||||
if st.checkbox(emp, key=key):
|
}, key=lambda value: int(value))
|
||||||
selected_employees.append(emp)
|
available_months[emp] = months
|
||||||
|
if emp not in st.session_state.employee_months:
|
||||||
|
st.session_state.employee_months[emp] = set(months[-2:])
|
||||||
|
|
||||||
|
# Mitarbeiter- und Monatsliste teilen sich die verfügbare Breite.
|
||||||
|
employee_col, month_col = st.columns(2)
|
||||||
|
with employee_col:
|
||||||
|
employee_btn_col1, employee_btn_col2 = st.columns(2)
|
||||||
|
if employee_btn_col1.button("☑ Alle auswählen", use_container_width=True):
|
||||||
|
for emp in employees:
|
||||||
|
st.session_state[f"sel_{emp}"] = True
|
||||||
|
if employee_btn_col2.button("☐ Keine auswählen", use_container_width=True):
|
||||||
|
for emp in employees:
|
||||||
|
st.session_state[f"sel_{emp}"] = False
|
||||||
|
|
||||||
|
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] = False
|
||||||
|
selected_months = st.session_state.employee_months.get(emp, set())
|
||||||
|
short_months = ", ".join(month_labels[m] for m in available_months[emp] if m in selected_months)
|
||||||
|
label = f"{emp} ({short_months})" if short_months else f"{emp} (**keine Monate**)"
|
||||||
|
if st.checkbox(
|
||||||
|
label,
|
||||||
|
key=key,
|
||||||
|
on_change=set_active_employee,
|
||||||
|
args=(emp,),
|
||||||
|
):
|
||||||
|
selected_employees.append(emp)
|
||||||
|
|
||||||
|
with month_col:
|
||||||
|
selected_for_months = st.session_state.get("active_month_employee")
|
||||||
|
if selected_for_months not in employees:
|
||||||
|
selected_for_months = None
|
||||||
|
|
||||||
|
month_btn_col1, month_btn_col2 = st.columns(2)
|
||||||
|
month_btn_col1.button(
|
||||||
|
"☑ Alle Monate",
|
||||||
|
use_container_width=True,
|
||||||
|
disabled=not selected_for_months,
|
||||||
|
on_click=set_all_employee_months,
|
||||||
|
args=(selected_for_months, available_months[selected_for_months], True) if selected_for_months else ("", [], True),
|
||||||
|
)
|
||||||
|
month_btn_col2.button(
|
||||||
|
"☐ Keine Monate",
|
||||||
|
use_container_width=True,
|
||||||
|
disabled=not selected_for_months,
|
||||||
|
on_click=set_all_employee_months,
|
||||||
|
args=(selected_for_months, available_months[selected_for_months], False) if selected_for_months else ("", [], False),
|
||||||
|
)
|
||||||
|
|
||||||
|
with st.container(border=True):
|
||||||
|
st.caption("Monate für den angeklickten Mitarbeiter")
|
||||||
|
if selected_for_months:
|
||||||
|
st.write(selected_for_months)
|
||||||
|
month_values = {}
|
||||||
|
for month in available_months[selected_for_months]:
|
||||||
|
month_key = f"month_{selected_for_months}_{month}"
|
||||||
|
if month_key not in st.session_state:
|
||||||
|
st.session_state[month_key] = month in st.session_state.employee_months[selected_for_months]
|
||||||
|
month_values[month] = st.checkbox(
|
||||||
|
f"{month_labels[month]} ({month})",
|
||||||
|
key=month_key,
|
||||||
|
on_change=sync_employee_months,
|
||||||
|
args=(selected_for_months, available_months[selected_for_months]),
|
||||||
|
)
|
||||||
|
st.session_state.employee_months[selected_for_months] = {
|
||||||
|
month for month, checked in month_values.items() if checked
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
st.info("Einen Mitarbeiter links auswählen, um seine Monate festzulegen.")
|
||||||
|
|
||||||
st.caption(f"{len(selected_employees)} von {len(employees)} Mitarbeitern ausgewählt")
|
st.caption(f"{len(selected_employees)} von {len(employees)} Mitarbeitern ausgewählt")
|
||||||
else:
|
else:
|
||||||
@@ -283,7 +429,20 @@ if generate_clicked:
|
|||||||
else:
|
else:
|
||||||
# Filtere Records auf selektierte Mitarbeiter
|
# Filtere Records auf selektierte Mitarbeiter
|
||||||
all_groups = group_by_employee(st.session_state.parsed_records)
|
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}
|
selected_groups = {
|
||||||
|
name: [
|
||||||
|
record for record in all_groups[name]
|
||||||
|
if record.monat.split(".", 1)[0].zfill(2)
|
||||||
|
in st.session_state.employee_months.get(name, set())
|
||||||
|
]
|
||||||
|
for name in selected_employees
|
||||||
|
if name in all_groups
|
||||||
|
}
|
||||||
|
selected_groups = {name: records for name, records in selected_groups.items() if records}
|
||||||
|
|
||||||
|
if not selected_groups:
|
||||||
|
st.error("Für die ausgewählten Mitarbeiter sind keine Monate ausgewählt.")
|
||||||
|
st.stop()
|
||||||
|
|
||||||
template_path = Path(app_config.template_dir) / selected_template
|
template_path = Path(app_config.template_dir) / selected_template
|
||||||
output_dir = Path(app_config.output_dir)
|
output_dir = Path(app_config.output_dir)
|
||||||
@@ -328,3 +487,10 @@ if "generated_files" in st.session_state and st.session_state.generated_files:
|
|||||||
help="Ordner im Finder öffnen",
|
help="Ordner im Finder öffnen",
|
||||||
use_container_width=True,
|
use_container_width=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if current_version_text:
|
||||||
|
st.markdown(
|
||||||
|
f'<div style="text-align:right; color:#888; font-size:0.8em; margin-top:2em;">'
|
||||||
|
f'Programmversion {current_version_text}</div>',
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""Prüft auf neuere Programmversionen im zentralen SMB-Verzeichnis."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
UPDATE_DIR = Path(r"j:\Fachbereiche\BO\Produktionsschule\Maßnahmeunterlagen\20_Personal\05_Tätigkeitsnachweise ESF\Nachweise-automatisiert-generieren")
|
||||||
|
VERSION_PATTERN = re.compile(r"Tätigkeitsnachweis-Generator v(\d+)\.(\d+)\.(\d+)(?:\.\d+)?\.(zip|exe)$", re.IGNORECASE)
|
||||||
|
CHANGELOG_PATTERN = re.compile(r"^## \[(\d+)\.(\d+)(?:\.(\d+))?\]", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_version():
|
||||||
|
match = CHANGELOG_PATTERN.search(Path(__file__).with_name("CHANGELOG.md").read_text(encoding="utf-8"))
|
||||||
|
current = tuple(int(part or 0) for part in match.groups()) if match else (0, 0, 0)
|
||||||
|
try:
|
||||||
|
available = UPDATE_DIR.exists()
|
||||||
|
except OSError:
|
||||||
|
available = False
|
||||||
|
if not available:
|
||||||
|
return current, None
|
||||||
|
newer = []
|
||||||
|
try:
|
||||||
|
paths = UPDATE_DIR.iterdir()
|
||||||
|
for path in paths:
|
||||||
|
found = VERSION_PATTERN.fullmatch(path.name)
|
||||||
|
if found:
|
||||||
|
version = tuple(int(found.group(i)) for i in range(1, 4))
|
||||||
|
if version > current:
|
||||||
|
newer.append((version, path))
|
||||||
|
except OSError:
|
||||||
|
return current, None
|
||||||
|
if newer:
|
||||||
|
return current, max(newer)
|
||||||
|
return current, None
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
current, latest = get_latest_version()
|
||||||
|
print(f"[INFO] Lokale Version: v{'.'.join(map(str, current))}")
|
||||||
|
if latest:
|
||||||
|
version, path = latest
|
||||||
|
print(f"[HINWEIS] Neuere Version verfügbar: v{'.'.join(map(str, version))}")
|
||||||
|
print(f"[HINWEIS] Datei: {path}")
|
||||||
|
else:
|
||||||
|
print("[INFO] Keine neuere Version gefunden oder Update-Verzeichnis nicht erreichbar.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -1 +1 @@
|
|||||||
1.2.2
|
1.3
|
||||||
@@ -222,6 +222,12 @@ def _prepare_build_dir() -> None:
|
|||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# README.md aus 0_application/ kopieren
|
||||||
|
readme_src = APP_DIR / "README.md"
|
||||||
|
if readme_src.exists():
|
||||||
|
shutil.copy2(readme_src, BUILD_OUTPUT_DIR / "README.md")
|
||||||
|
print(" ✓ README.md nach 2_build/ kopiert")
|
||||||
|
|
||||||
print(f" ✓ 2_build/ vorbereitet")
|
print(f" ✓ 2_build/ vorbereitet")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user