27 Commits
v1.1.0 ... main

Author SHA1 Message Date
2f79c9e650 feat: add optional matomo tracking for pageviews and download 2026-03-03 09:57:29 +00:00
0d7ae8bbb7 feat: add release workflow and update docs 2026-03-03 09:40:18 +00:00
6f9533ec65 chore: switch docker image source to gitea registry 2026-03-03 09:23:57 +00:00
f98d75627d chore: bump default image tag to v1.2.2 2026-03-03 09:08:29 +00:00
01b6b7d6ce fix: fallback to local image when pull fails 2026-03-03 08:51:03 +00:00
394a5fc234 chore: remove non-docker artifacts and docs 2026-03-03 08:47:19 +00:00
e888772488 chore: simplify docker-only deployment workflow 2026-03-03 08:43:30 +00:00
2b9476290f Add iPD export images to landing page 2026-03-03 09:14:55 +01:00
144d13989d Add landing page in front of web converter 2026-03-02 22:49:51 +01:00
02b005376f Fix preview filters and button reset on reload 2026-03-02 22:18:47 +01:00
9e5127a867 Add event preview with two-step download flow 2026-03-02 22:03:11 +01:00
f46763ace7 Add Docker deployment setup for web app 2026-03-02 20:27:53 +01:00
552830acef Add optional Nginx IP whitelist hardening 2026-03-02 20:22:33 +01:00
2c99a75cd8 Add optional app-level basic auth via env vars 2026-03-02 20:21:30 +01:00
158ef648ee Document Nginx Basic Auth setup for web deployment 2026-03-02 20:18:54 +01:00
06bce55514 Add HTTPS deployment guide for web MVP 2026-03-02 20:18:06 +01:00
b14cc39455 Add browser-based web MVP with mobile upload flow 2026-03-02 20:17:44 +01:00
e7c62c2628 Add Gitea workflow for cross-platform standalone builds 2026-03-02 18:25:45 +01:00
17b2e7ed59 Bump version to 1.2.2 2026-03-02 18:03:59 +01:00
54516a315a Restructure README with OS install and standalone guidance 2026-03-02 18:03:21 +01:00
1bb8820cf1 Bump version to 1.2.1 2026-03-02 17:57:19 +01:00
95c461eca4 Add standalone build and packaging workflow docs/scripts 2026-03-02 17:56:13 +01:00
db76fbf0d2 Release 1.2.0: wxPython migration + vacation exclusion 2026-03-02 17:37:10 +01:00
07c8905f47 feat: Add Drag & Drop support to wxPython GUI
- Implement FileDropTarget class for native file drop handling
- Enable drag and drop for PDF files in the ListBox
- Filter to accept only PDF files with proper validation
- Add user feedback messages when files are dropped
- Store directory of dropped files for next file dialog
2026-02-26 10:33:53 +01:00
e692983a02 Füge native wxPython GUI hinzu
- Erstelle gui_wxpython.py mit vollständiger nativer GUI-Integration
- Funktioniert auf macOS 13.6 (im Gegensatz zu BeeWare/Toga)
- Native Cocoa-Widgets auf macOS, Win32 auf Windows, GTK auf Linux
- Alle Features der Tkinter-Version vollständig implementiert
- Automatische Dark Mode Unterstützung
- Thread-sichere UI-Updates mit wx.CallAfter
- Native File-Dialoge und Menüleiste
- Füge WXPYTHON_README.md mit vollständiger Dokumentation hinzu
- Emojis aus Buttons entfernt für zuverlässige Darstellung
- Einheitliches Button-Styling
2026-02-26 10:20:03 +01:00
df3db2cba4 Add packaging dependency and fallback version compare 2026-02-24 14:57:19 +01:00
61971f2155 docs: improve Windows install and troubleshooting guide 2026-02-24 14:10:27 +01:00
37 changed files with 1300 additions and 2388 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
*
!pdf_to_ics.py
!web/
!web/**

16
.env.example Normal file
View File

@@ -0,0 +1,16 @@
# Docker-only Deployment Konfiguration
# Kopieren nach .env und Werte anpassen:
# cp .env.example .env
# Container-Image mit festem Release-Tag (kein latest)
PDF_TO_ICS_IMAGE=git.file-archive.de/webfarben/pdf_to_ics:v1.2.2
# Optional: App-interne Basic Auth (leer = deaktiviert)
WEB_AUTH_USER=
WEB_AUTH_PASSWORD=
# Optional: Matomo Tracking
# Beispiel: MATOMO_URL=https://matomo.example.de
# Beispiel: MATOMO_SITE_ID=5
MATOMO_URL=
MATOMO_SITE_ID=

View File

@@ -0,0 +1,65 @@
name: Build Standalone Releases
on:
workflow_dispatch:
push:
tags:
- "v*"
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: linux
runs-on: ubuntu-latest
shell: bash
build_cmd: |
chmod +x build/build_linux.sh build/package_linux.sh
./build/build_linux.sh
./build/package_linux.sh
artifact_glob: release/PDFtoICS-linux-v*.tar.gz
- os: macos
runs-on: macos-latest
shell: bash
build_cmd: |
chmod +x build/build_macos.sh build/package_macos.sh
./build/build_macos.sh
./build/package_macos.sh
artifact_glob: release/PDFtoICS-macos-v*.zip
- os: windows
runs-on: windows-latest
shell: pwsh
build_cmd: |
cmd /c build\build_windows.cmd
cmd /c build\package_windows.cmd
artifact_glob: release/PDFtoICS-windows-v*.zip
runs-on: ${{ matrix.runs-on }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Create venv
shell: ${{ matrix.shell }}
run: |
python -m venv .venv
- name: Build + Package
shell: ${{ matrix.shell }}
run: ${{ matrix.build_cmd }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-release
path: ${{ matrix.artifact_glob }}

8
.gitignore vendored
View File

@@ -3,3 +3,11 @@
.venv/
__pycache__/
.pdf_to_ics_config.json
.env
.env.local
# Build artifacts
dist/
release/
*.spec
build/PDFtoICS/

16
Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY web/requirements-web.txt /app/web/requirements-web.txt
RUN pip install --no-cache-dir -r /app/web/requirements-web.txt
COPY pdf_to_ics.py /app/pdf_to_ics.py
COPY web /app/web
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "web.app:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -1,83 +0,0 @@
# 🎨 GUI Installation
Die grafische Benutzeroberfläche benötigt Tkinter, das auf manchen Systemen separat installiert werden muss.
## Installation von Tkinter
### Ubuntu/Debian
```bash
sudo apt-get update
sudo apt-get install python3-tk
```
### Fedora/RHEL
```bash
sudo dnf install python3-tkinter
```
### Arch Linux
```bash
sudo pacman -S tk
```
### macOS
Tkinter ist bereits mit Python installiert - nichts zu tun! ✓
### Windows
Tkinter ist bereits mit Python installiert - nichts zu tun! ✓
## GUI starten
Nach der Tkinter-Installation:
**Linux/macOS:**
```bash
./start_gui.sh
```
**Windows:**
```
Doppelklick auf start_gui.cmd
```
## GUI-Features
**Drag & Drop:** Ziehen Sie PDF-Dateien direkt in die Liste (optional mit tkinterdnd2)
📋 **Mehrere PDFs:** Wählen Sie mehrere Dateien gleichzeitig
📁 **Ausgabe-Verzeichnis:** Wählen Sie, wo die ICS-Dateien gespeichert werden
📊 **Echtzeit-Log:** Sehen Sie den Fortschritt live
**Fortschrittsbalken:** Visuelles Feedback bei der Konvertierung
### Drag & Drop aktivieren (optional)
Für besseres Drag & Drop installieren Sie tkinterdnd2:
```bash
.venv/bin/pip install tkinterdnd2
```
Oder lassen Sie das Startskript es automatisch installieren.
## Fehlerbehebung
### "No module named 'tkinter'"
→ Tkinter muss installiert werden (siehe oben)
### GUI startet nicht
→ Versuchen Sie:
```bash
rm -rf .venv
./start_gui.sh
```
### Fenster erscheint nicht
→ Stellen Sie sicher, dass Sie eine grafische Oberfläche haben (kein SSH ohne X11)
## Alternative: CLI-Version
Falls Tkinter nicht installiert werden kann, nutzen Sie die CLI-Version:
```bash
./start.sh
```
Die CLI-Version funktioniert überall ohne zusätzliche Installation! 🚀

View File

@@ -1,159 +0,0 @@
# 📦 Installation - PDF zu ICS Konverter
Eine einfache Installation für Linux-Systeme, die die Anwendung in Ihr Anwendungsmenü integriert.
## 🚀 Schnell-Installation
```bash
chmod +x install.sh
./install.sh
```
Das war's! Die Anwendung erscheint nun in Ihrem Anwendungsmenü unter "PDF zu ICS Konverter".
## 📋 Was macht das Installations-Script?
1.**Prüft Python-Installation** (Python 3.6+)
2.**Installiert Tkinter** falls nötig (mit sudo-Berechtigung)
3.**Erstellt Installationsverzeichnis** in `~/.local/share/pdf-to-ics`
4.**Kopiert alle Dateien** ins Installationsverzeichnis
5.**Erstellt Python Virtual Environment** mit allen Abhängigkeiten
6.**Erstellt Desktop-Verknüpfung** für das Anwendungsmenü
7.**Erstellt Launcher-Script** in `~/.local/bin/pdf-to-ics`
## 🎯 Nach der Installation
Die Anwendung starten Sie auf drei Arten:
### 1. Über das Anwendungsmenü (Empfohlen)
- Öffnen Sie Ihr Anwendungsmenü (z.B. GNOME Activities, KDE Application Launcher)
- Suchen Sie nach "PDF zu ICS"
- Klicken Sie auf das Icon
### 2. Über die Kommandozeile
```bash
pdf-to-ics
```
### 3. Über den vollständigen Pfad
```bash
~/.local/bin/pdf-to-ics
```
## 🔧 Systemanforderungen
### Unterstützte Distributionen:
- ✅ Ubuntu / Debian (automatische Tkinter-Installation)
- ✅ Fedora / RHEL (automatische Tkinter-Installation)
- ✅ Arch Linux (automatische Tkinter-Installation)
- ✅ Andere Distributionen (manuelle Tkinter-Installation erforderlich)
### Voraussetzungen:
- Python 3.6 oder höher
- `sudo`-Berechtigung (für Tkinter-Installation)
- Etwa 50 MB Festplattenspeicher
## 📁 Installations-Pfade
```
~/.local/share/pdf-to-ics/ # Hauptinstallation
~/.local/bin/pdf-to-ics # Launcher-Script
~/.local/share/applications/ # Desktop-Verknüpfung
~/.pdf_to_ics_config.json # Benutzer-Einstellungen
```
## 🗑️ Deinstallation
```bash
~/.local/share/pdf-to-ics/uninstall.sh
```
Das Deinstallations-Script entfernt:
- ✅ Installationsverzeichnis
- ✅ Desktop-Verknüpfung
- ✅ Launcher-Script
- ⚠️ Konfigurationsdatei (optional)
## ⚠️ Fehlerbehebung
### "Tkinter ist nicht installiert"
**Ubuntu/Debian:**
```bash
sudo apt-get install python3-tk
```
**Fedora:**
```bash
sudo dnf install python3-tkinter
```
**Arch Linux:**
```bash
sudo pacman -S tk
```
### "pdf-to-ics: Befehl nicht gefunden"
Ihr `~/.local/bin` ist nicht im PATH. Fügen Sie zu `~/.bashrc` hinzu:
```bash
export PATH="$HOME/.local/bin:$PATH"
```
Dann Terminal neu laden:
```bash
source ~/.bashrc
```
### Anwendung erscheint nicht im Menü
Aktualisieren Sie die Desktop-Datenbank:
```bash
update-desktop-database ~/.local/share/applications
```
Oder melden Sie sich ab und wieder an.
## 🔄 Update
Um auf eine neue Version zu aktualisieren:
```bash
# 1. Deinstallieren
~/.local/share/pdf-to-ics/uninstall.sh
# 2. Neue Version herunterladen
cd /pfad/zur/neuen/version
# 3. Neu installieren
./install.sh
```
## 💡 Entwickler-Modus
Wenn Sie an der Anwendung entwickeln möchten, nutzen Sie stattdessen:
```bash
./start_gui.sh # Startet aus dem aktuellen Verzeichnis
```
Die Installation ist nur für End-Benutzer gedacht.
## 🐧 Andere Betriebssysteme
- **Windows:** Nutzen Sie `start_gui.cmd` (keine Installation nötig)
- **macOS:** Nutzen Sie `start_gui.sh` (keine Installation nötig)
## 📞 Support
Bei Problemen während der Installation:
1. Prüfen Sie die Systemanforderungen
2. Lesen Sie die Fehlermeldungen sorgfältig
3. Konsultieren Sie die README.md
4. Öffnen Sie ein Issue im Repository
---
**Installation erfolgreich?** Viel Spaß beim Konvertieren Ihrer Dienstpläne! 📅✨

View File

@@ -1,98 +0,0 @@
# 🚀 Quick Start Guide
## Schnellstart - 3 Schritte
### 1. Programm starten
**macOS/Linux:**
```bash
./start.sh
```
**Windows:**
Doppelklick auf `start.cmd`
### 2. PDF-Dateien hinzufügen
Kopieren Sie Ihre Dienstplan-PDF-Dateien in dieses Verzeichnis:
```
/home/sebastian/Dokumente/ICS-Import/
```
### 3. Konvertieren und Importieren
Im Menü wählen Sie Option "1. PDF(s) konvertieren" und die ICS-Dateien werden automatisch erstellt.
---
## 📅 In deinen Kalender importieren
### Google Kalender
1. Öffne [google.com/calendar](https://google.com/calendar)
2. Einstellungen → Kalender importieren
3. "Datei aussuchen" → `.ics` Datei auswählen
4. Importieren
### Outlook
1. Öffne Outlook
2. Datei → Öffnen und exportieren → Importieren
3. `.ics` Datei auswählen
4. In einen Kalender importieren
### Thunderbird/SeaMonkey
1. Kalender öffnen
2. Datei → Importieren
3. `.ics` Datei auswählen
### Apple Kalender (macOS/iOS)
1. Doppelklick auf die `.ics` Datei
2. Bestätigen Sie den Import
### Linux (KDE Kontact, etc.)
1. Öffnen Sie die Kalenderanwendung
2. Datei → Importieren
3. `.ics` Datei auswählen
---
## 💡 Tipps und Tricks
### Mehrere PDFs gleichzeitig
Das Programm verarbeitet automatisch alle `.pdf` Dateien im Verzeichnis. Kopieren Sie einfach mehrere PDFs hinein!
### Zeitzone anpassen
Falls Sie eine andere Zeitzone benötigen, bearbeiten Sie `pdf_to_ics.py`:
```python
tz = pytz.timezone('Europe/Berlin') # Ändern Sie diesen Wert
```
Verfügbare Zeitzonen: [Liste hier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
### Automatische Updates
Sobald Sie neue Dienstplan-PDFs hinzufügen und das Programm erneut ausführen, werden neue `.ics` Dateien erstellt.
---
## ❓ Häufige Probleme
### "Keine PDF-Dateien gefunden"
- Überprüfen Sie, dass die PDF-Dateien im gleichen Verzeichnis wie die Skripte sind
- Dateiname darf Leerzeichen enthalten
### "Keine Events gefunden"
- Die PDF muss das richtige Format haben (ein Dienstplan mit Tabelle)
- Kontrollieren Sie die PDF-Struktur
### Zeitangaben falsch
- Die Standard-Einstellung ist Zeitzone "Europe/Berlin"
- Falls Sie eine andere Zeitzone benötigen, siehe "Zeitzone anpassen" oben
---
## 📞 Weitere Hilfe
Siehe **README.md** für ausführliche Dokumentation.
---
Viel Spaß! 😊

246
README.md
View File

@@ -1,245 +1,51 @@
# PDF zu ICS Konverter - Dienstplan Importer
# PDF zu ICS Docker/Web Only
Dieses Tool extrahiert Kalenderdaten aus Dienstplan-PDFs und konvertiert sie in das iCalendar-Format (ICS), das von den meisten Kalenderanwendungen importiert werden kann.
Dieses Repository ist auf den Web-Betrieb im Docker-Container reduziert.
## 🎯 Zwei Versionen verfügbar
### 1. **GUI-Version** (Grafische Oberfläche) - Empfohlen!
Benutzerfreundliche grafische Oberfläche mit Drag & Drop Support.
## Schnellstart
```bash
./start_gui.sh
cp .env.example .env
./deploy.sh
```
**Features:**
- ✨ Drag & Drop für PDF-Dateien
- 📋 Mehrere PDFs gleichzeitig verarbeiten
- 📁 Ausgabe-Verzeichnis frei wählbar
- 📊 Live-Log und Fortschrittsanzeige
- 💾 Merkt sich letzte Verzeichnisse
Danach erreichbar unter:
- `http://<SERVER-IP>:8000`
- `http://<SERVER-IP>:8000/app`
**Voraussetzung:** Tkinter muss installiert sein (siehe [GUI_README.md](GUI_README.md))
### 2. **CLI-Version** (Kommandozeile)
Textbasiertes Menü für die Kommandozeile.
## Update
```bash
./start.sh
./update.sh
```
**Features:**
- ⚡ Funktioniert überall ohne zusätzliche Abhängigkeiten
- 🔄 Automatische Verarbeitung aller PDFs im Verzeichnis
- 📝 Textbasiertes interaktives Menü
## Features
✅ Extrahiert Dienstplan-Informationen aus PDFs
✅ Erkennt Schicht-Zeitangaben (z.B. 04:51-15:46)
✅ Handhabt Nachtschichten korrekt (über Mitternacht hinaus)
✅ Erstellt Standard-konforme ICS-Dateien
✅ Unterstützt mehrere PDFs gleichzeitig
✅ GUI mit Drag & Drop (optional)
✅ CLI-Menü für schnelle Nutzung
## Installation
### Schnellstart (Empfohlen)
**Für GUI-Version:**
```bash
./start_gui.sh
```
Siehe [GUI_README.md](GUI_README.md) für Tkinter-Installation.
**Für CLI-Version:**
```bash
./start.sh
```GUI-Version (Empfohlen)
1. Starten Sie die GUI:
```bash
./start_gui.sh
```
2. Fügen Sie PDF-Dateien hinzu:
- Klicken Sie auf " PDF hinzufügen", oder
- Ziehen Sie PDF-Dateien in die Liste (Drag & Drop)
**Interaktives Menü:**
## Neues Release bauen & pushen
```bash
./start.sh
./release.sh v1.2.3
```
Dann wählen Sie im Menü die gewünschte Option.
### Erweiterte Nutzung (Python-Modul)abe-Verzeichnis (optional)
4. Klicken Sie auf "📄 ICS Datei erstellen"
Die GUI merkt sich Ihre letzten Verzeichnisse für schnelleren Zugriff!
### CLI-Version
**Schnellstart:**
Beide Skripte erstellen automatisch eine Python Virtual Environment und installieren alle benötigten Abhängigkeiten.
### Manuelle Installation
Die erforderlichen Dependencies sind bereits installiert. Falls Sie das Projekt neu einrichten:
Optional direkt `.env` auf den neuen Tag setzen und deployen:
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install pdfplumber icalendar pypdf2 pytz
./release.sh v1.2.3 --set-env --deploy
```
## Verwendung
Für ein neues Release zuerst den Tag in `.env` anpassen:
### Schnellstart (CLI)
1. Kopieren Sie Ihre Dienstplan-PDF-Dateien in ein Verzeichnis
2. Führen Sie das Skript aus:
```bash
python3 pdf_to_ics.py
```dotenv
PDF_TO_ICS_IMAGE=git.file-archive.de/webfarben/pdf_to_ics:v1.2.2
```
Das Tool findet automatisch alle `.pdf` Dateien im aktuellen Verzeichnis und erstellt entsprechende `.ics` Dateien.
## Enthaltene Betriebsdateien
### Kommandozeilen-Optionen
- `docker-compose.deploy.yml` image-basiertes Deployment
- `.env.example` Konfigurationsvorlage
- `deploy.sh` Erststart
- `update.sh` Update-Workflow
- `release.sh` Build + Push für neue Image-Tags
- `WEB_README.md` ausführliche Web-/Server-Doku
```bash
# Alle PDFs im aktuellen Verzeichnis konvertieren
python3 pdf_to_ics.py
## Hinweis
# PDFs aus einem bestimmten Verzeichnis konvertieren
python3 pdf_to_ics.py --input ./pdfs
# PDFs in anderes Verzeichnis speichern
python3 pdf_to_ics.py --input ./pdfs --output ./ics_dateien
# Ruhetage ausschließen
python3 pdf_to_ics.py --exclude-rest
# Einzelne PDF-Datei konvertieren
python3 pdf_to_ics.py /pfad/zur/datei.pdf
# Mit detaillierter Ausgabe
python3 pdf_to_ics.py --input ./pdfs -v
# Hilfe anzeigen
python3 pdf_to_ics.py --help
```
**Verfügbare Optionen:**
| Option | Kurzform | Beschreibung |
|--------|----------|-------------|
| `--input DIR` | `-i` | Eingabe-Verzeichnis mit PDF-Dateien (Standard: aktuelles Verzeichnis) |
| `--output DIR` | `-o` | Ausgabe-Verzeichnis für ICS-Dateien (Standard: Eingabe-Verzeichnis) |
| `--exclude-rest` | `-e` | Ruhetage ausschließen (Ruhe, R56, R36, vRWF48, RWE, vR48) |
| `--verbose` | `-v` | Detaillierte Ausgabe anzeigen |
| `--help` | `-h` | Hilfe anzeigen |
### Erweiterte Nutzung
Sie können auch direkt mit dem Python-Modul arbeiten:
```python
from pdf_to_ics import extract_dienstplan_data, create_ics_from_dienstplan
# PDF verarbeiten
dienstplan = extract_dienstplan_data('meine_pdf.pdf')
# ICS erstellen
create_ics_from_dienstplan(dienstplan, 'mein_kalender.ics')
```
## Dateiformat
### ICS-Datei importieren
Die erstellte `.ics` Datei kann in folgende Kalenderanwendungen importiert werden:
- **Outlook**: Datei → Öffnen und exportieren → Importieren
- **Google Kalender**: Einstellungen → Kalender importieren
- **iCal/macOS**: Doppelklick auf die .ics Datei
- **Thunderbird**: Kalender → Kalender importieren
- **Android**: Mit einer Kalender-App öffnen
- **LibreOffice**: Datei → Öffnen
## Struktur der extrahierten Daten
Das Tool extrahiert folgende Informationen aus der PDF:
- **Name und Personalnummer**: Des Mitarbeiters
- **Betriebshof**: Standort
- **Sollarbeitszeit**: Gewünschte Arbeitszeit pro Monat
- **Events**: Einzelne Schichten mit:
- Datum
- Dienstart (z.B. "36234", "Ruhe", "Dispo")
- Zeitangabe (falls vorhanden)
## Output
Für jede verarbeitete PDF wird eine entsprechende ICS-Datei erstellt:
```
2026-02-23 DB Köhler00100718_März2026.pdf → 2026-02-23 DB Köhler00100718_März2026.ics
```
Die ICS-Datei enthält ein Event für jeden Arbeitstag mit:
- **Titel**: Name - Dienstart
- **Beschreibung**: Dienstart und Betriebshof
- **Zeit**: Mit aktueller Zeitzone (Europe/Berlin)
## Fehlerbehebung
### Keine Events gefunden?
- Stellen Sie sicher, dass die PDF das erwartete Tabellenformat hat
- Überprüfen Sie die Dateiname und die PDF-Struktur
### Zeitzone falsch?
- Die aktuelle Einstellung ist Europe/Berlin
- Zum Ändern: Bearbeiten Sie die Zeile in `pdf_to_ics.py`:
```python
tz = pytz.timezone('Europe/Berlin') # Ändern SIe diesen Wert
```
## Technische Details
### Projektstruktur
```
ICS-Import/
├── pdf_to_ics.py # Core-Konvertierungslogik
├── gui.py # GUI-Version (Tkinter)
├── menu.py # CLI-Menü
├── start_gui.sh/cmd # GUI-Startskripte
├── start.sh/cmd # CLI-Startskripte
├── README.md # Diese Datei
└── GUI_README.md # GUI-spezifische Dokumentation
```
### Technische Spezifikationen
- **Abhängigkeiten**: pdfplumber, icalendar, pytz, pypdf2
- **Optional für GUI**: tkinter (Python-Standard), tkinterdnd2 (Drag & Drop)
- **Python-Version**: 3.6+
- **Format**: iCalendar 2.0 (RFC 5545)
- **Konfiguration**: `~/.pdf_to_ics_config.json` (GUI-Einstellungen)
## Lizenz
Dieses Tool ist zur privaten Verwendung gedacht.
---
## 📚 Weitere Dokumentation
- **[GUI_README.md](GUI_README.md)** - Ausführliche GUI-Dokumentation und Tkinter-Installation
- **[QUICKSTART.md](QUICKSTART.md)** - Schnellanleitung für den Import in verschiedene Kalender
- **[ZUSAMMENFASSUNG.md](ZUSAMMENFASSUNG.md)** - Projekt-Übersicht und technische Details
GUI-/CLI-Varianten wurden bewusst entfernt, da der Betrieb ausschließlich im Docker-Container erfolgt.

103
WEB_README.md Normal file
View File

@@ -0,0 +1,103 @@
# 🌐 Web-Version (Docker-only)
Diese Anwendung wird ausschließlich als Container betrieben.
## Voraussetzungen
- Docker + Docker Compose
- Optional externes Netzwerk `proxy` (wenn Reverse Proxy genutzt wird)
## Erststart
```bash
cp .env.example .env
./deploy.sh
```
## Update
```bash
./update.sh
```
## Release (neues Image bauen + pushen)
```bash
./release.sh v1.2.3
```
Optional direkt `.env` auf das neue Tag setzen und deployen:
```bash
./release.sh v1.2.3 --set-env --deploy
```
Für ein neues Release den Tag in `.env` erhöhen, z. B.:
```dotenv
PDF_TO_ICS_IMAGE=git.file-archive.de/webfarben/pdf_to_ics:v1.2.2
```
Danach `./update.sh` ausführen.
Hinweis: `deploy.sh` und `update.sh` versuchen zuerst einen Registry-Pull.
Wenn der Pull fehlschlägt, wird automatisch mit einem bereits lokal vorhandenen Image gleichen Tags weitergemacht.
## Manuelle Kommandos (optional)
Starten:
```bash
docker compose -f docker-compose.deploy.yml up -d
```
Status/Logs:
```bash
docker compose -f docker-compose.deploy.yml ps
docker compose -f docker-compose.deploy.yml logs -f pdf-to-ics-web
```
Stoppen:
```bash
docker compose -f docker-compose.deploy.yml down
```
## Sicherheit
Optional App-Basic-Auth in `.env` setzen:
```dotenv
WEB_AUTH_USER=kalender
WEB_AUTH_PASSWORD=BitteSicheresPasswortSetzen
```
Für öffentliches Deployment zusätzlich Reverse Proxy + HTTPS verwenden.
## Matomo (optional)
Für Zugriffsstatistiken kannst du Matomo direkt aktivieren:
```dotenv
MATOMO_URL=https://matomo.example.de
MATOMO_SITE_ID=5
```
Dann neu deployen:
```bash
./update.sh
```
Wenn beide Variablen gesetzt sind, trackt die App Seitenaufrufe auf Landing/App sowie ein Event für den ICS-Download.
## Schlanker Server-Checkout (Sparse)
```bash
git clone --filter=blob:none --sparse <REPO_URL> pdf_to_ics
cd pdf_to_ics
git sparse-checkout set docker-compose.deploy.yml .env.example deploy.sh update.sh release.sh README.md WEB_README.md
```
Hinweis: Ein bestehender Voll-Clone wird dadurch nicht automatisch kleiner.

View File

@@ -1,88 +0,0 @@
# 📦 PDF zu ICS Konverter - Weitergabe-Paket
Dieses Paket enthält alles, was Sie brauchen, um Dienstplan-PDFs in Kalender-Dateien (ICS) zu konvertieren.
## ⚡ Schnellstart für Nicht-Technische Nutzer
### 1. Installation
1. Öffnen Sie diesen Ordner im Dateimanager
2. Rechtsklick auf `install.sh`
3. Wählen Sie "Als Programm ausführen" oder "Mit Terminal öffnen"
4. Folgen Sie den Anweisungen auf dem Bildschirm
**Alternativ:** Öffnen Sie ein Terminal in diesem Ordner und führen Sie aus:
```bash
./install.sh
```
### 2. Anwendung starten
Nach der Installation finden Sie "PDF zu ICS Konverter" in Ihrem Anwendungsmenü:
- **GNOME:** Drücken Sie die Super-Taste, tippen Sie "PDF"
- **KDE:** Klicken Sie auf das Anwendungsmenü, suchen Sie "PDF zu ICS"
- **XFCE/Andere:** Im Anwendungsmenü unter "Büro" oder "Dienstprogramme"
### 3. PDFs konvertieren
1. Klicken Sie auf " PDF hinzufügen" oder ziehen Sie PDF-Dateien in die Liste
2. Wählen Sie optional ein Ausgabe-Verzeichnis
3. Klicken Sie auf "📄 ICS Datei erstellen"
4. Fertig! Importieren Sie die ICS-Dateien in Ihren Kalender
## 📋 Was wird installiert?
- Die Anwendung wird in Ihrem Home-Verzeichnis installiert (keine Systemänderungen)
- Ein Eintrag im Anwendungsmenü wird erstellt
- Alle benötigten Python-Bibliotheken werden automatisch heruntergeladen
## 🗑️ Deinstallation
Falls Sie die Anwendung wieder entfernen möchten:
1. Öffnen Sie ein Terminal
2. Führen Sie aus:
```bash
~/.local/share/pdf-to-ics/uninstall.sh
```
## 💡 Für technisch versierte Nutzer
Siehe [INSTALL.md](INSTALL.md) für detaillierte Informations- und [README.md](README.md) für die vollständige Dokumentation.
## ⚙️ Systemvoraussetzungen
- Linux (Ubuntu, Fedora, Debian, Arch, etc.)
- Python 3.6 oder neuer (meist vorinstalliert)
- Etwa 50 MB freier Speicherplatz
- Internetzugang für die Installation
## ❓ Probleme?
### Die Installation schlägt fehl
Öffnen Sie ein Terminal und führen Sie aus:
```bash
python3 --version
```
Falls "Befehl nicht gefunden" erscheint, installieren Sie Python:
```bash
sudo apt install python3 python3-pip python3-venv
```
### Die Anwendung startet nicht
Prüfen Sie, ob Tkinter installiert ist:
```bash
sudo apt install python3-tk
```
## 📞 Weitere Hilfe
Lesen Sie die vollständige Dokumentation in [README.md](README.md) oder [INSTALL.md](INSTALL.md).
---
Viel Erfolg beim Konvertieren Ihrer Dienstpläne! 📅

View File

@@ -1,178 +0,0 @@
# 📋 Projekt-Zusammenfassung
## ✅ Projekt abgeschlossen!
Ihr PDF zu ICS Konverter für Dienstpläne ist vollständig eingerichtet und einsatzbereit.
---
## 📁 Projektstruktur
```
ICS-Import/
├── 📄 README.md ← ausführliche Dokumentation
├── 📄 QUICKSTART.md ← schnelle Anleitung
├── 📄 ZUSAMMENFASSUNG.md ← dieses Dokument
├── 🐍 pdf_to_ics.py ← Kern-Konvertierungsskript
├── 🎨 menu.py ← interaktives Benutzermenü
├── 🚀 start.sh ← Startskript (macOS/Linux)
├── 🚀 start.cmd ← Startskript (Windows)
├── 📦 .venv/ ← Python-Umgebung (automatisch)
└── 📝 PDF-Dateien & ICS-Dateien ← ihre Daten
├── 2026-02-23 DB Köhler00100718_März2026.pdf
└── 2026-02-23 DB Köhler00100718_März2026.ics ✓
```
---
## 🎯 Was wurde erstellt
### 1. **PDF-Parser** (`pdf_to_ics.py`)
- Extrahiert Dienstplan-Informationen aus PDFs
- Erkennt Zeitangaben und Schichtdaten
- Erstellt valide iCalendar-Dateien
- Unterstützt mehrere PDF-Dateien
### 2. **Benutzermenü** (`menu.py`)
- Interaktive Oberfläche
- PDF-Verzeichnis durchsuchen
- Mehrere PDFs konvertieren
- Fehlerbehandlung
### 3. **Startskripte**
- Linux/macOS: `start.sh`
- Windows: `start.cmd`
- Automatische Umgebungseinrichtung
### 4. **Dokumentation**
- README.md - ausführliche Anleitung
- QUICKSTART.md - schnelle Einstieg
- Diese Zusammenfassung
---
## 🎛️ Verwendung
### Von der Kommandozeile:
```bash
cd /home/sebastian/Dokumente/ICS-Import
python3 pdf_to_ics.py # Alle PDFs konvertieren
python3 menu.py # Interaktives Menü
./start.sh # Schnellstart (Linux/macOS)
```
### Von Windows:
```
Doppelklick auf start.cmd
```
---
## 📅 Funktionen der erstellten ICS-Dateien
**Vollständige Ereignisinformationen:**
- Titel: Name - Dienstart
- Beschreibung: Dienstart & Betriebshof
- Datum und Uhrzeit
- Zeitzone: Europe/Berlin
**Intelligente Zeitverarbeitung:**
- Nachtschichten (über Mitternacht)
- Ganztagesveranstaltungen
- Automatische Zeitzone-Anpassung
**Kalender-kompatibel mit:**
- Google Kalender ✓
- Outlook ✓
- Apple Kalender ✓
- Thunderbird ✓
- LibreOffice ✓
- Linux Kalender-Apps ✓
---
## 🔧 Technische Details
- **Sprache:** Python 3.6+
- **Abhängigkeiten:** pdfplumber, icalendar, pytz, pypdf2
- **Format:** iCalendar 2.0 (RFC 5545)
- **Umgebung:** Python virtual environment (.venv)
### Installierte Pakete:
```
pdfplumber==0.10.0+ - PDF-Datenextraktion
icalendar==5.0.0+ - ICS-Datei-Erstellung
pytz - Zeitzonenverwaltung
pypdf2 - PDF-Parsing
```
---
## 💡 Nächste Schritte
### 1. **Ihre erste Konvertierung:**
```bash
cd /home/sebastian/Dokumente/ICS-Import
./start.sh
```
Wählen Sie Option "1. PDF(s) konvertieren"
### 2. **ICS in Kalender importieren:**
- Siehe README.md für Anleitung pro Kalender-App
- Oder siehe QUICKSTART.md für schnelle Übersicht
### 3. **Weitere PDFs hinzufügen:**
- Kopieren Sie PDF-Dateien in das Verzeichnis
- Das Programm verarbeitet sie automatisch
---
## 🐛 Fehlerbehebung
| Problem | Lösung |
|---------|--------|
| "Keine PDF gefunden" | PDFs müssen im Projektverzeichnis sein |
| "Keine Events" | PDF muss das richtige Tabellenformat haben |
| "Zeitzone falsch" | Bearbeiten Sie `pdf_to_ics.py` Zeile mit `pytz.timezone()` |
| Import-Fehler in Kalender | Stelle sicher, dass die `.ics` Datei nicht beschädigt ist |
---
## 📊 Beispiel-Output
```
Verarbeite: 2026-02-23 DB Köhler00100718_März2026.pdf
Name: Sebastian Köhler
Personalnummer: 00100718
Betriebshof: NSCH3
Anzahl der Events: 31
✓ ICS-Datei erstellt: 2026-02-23 DB Köhler00100718_März2026.ics
```
Jedes Event in der ICS-Datei:
- **Datum:** Automatisch aus PDF extrahiert
- **Zeit:** z.B. 04:51-15:46 Uhr
- **Titel:** z.B. "Sebastian Köhler - 36234"
- **Beschreibung:** Dienstart & Betriebshof
---
## 📞 Support
Für detaillierte Informationen:
- **README.md** - Ausführliche Dokumentation
- **QUICKSTART.md** - Schnelle Anleitung zum Import
- **Python-Code** - Gut kommentiert und erweiterbar
---
## 🎉 Fertig!
Ihr System ist bereit. Viel Erfolg mit der Dienstplan-Verwaltung! 📅✨
**Letzte Änderung:** 23. Februar 2026
**Status:** ✅ Einsatzbereit

58
deploy.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
COMPOSE_FILE="docker-compose.deploy.yml"
if ! command -v docker >/dev/null 2>&1; then
echo "❌ Fehler: docker ist nicht installiert oder nicht im PATH."
exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
echo "❌ Fehler: docker compose ist nicht verfügbar."
exit 1
fi
if [ ! -f "$COMPOSE_FILE" ]; then
echo "❌ Fehler: $COMPOSE_FILE nicht gefunden."
exit 1
fi
if [ ! -f ".env" ]; then
if [ -f ".env.example" ]; then
cp .env.example .env
echo " .env wurde aus .env.example erstellt. Bitte Werte prüfen."
else
echo "❌ Fehler: .env fehlt und keine .env.example vorhanden."
exit 1
fi
fi
IMAGE_REF="$(docker compose -f "$COMPOSE_FILE" config | awk '/image:/{print $2; exit}')"
if [ -z "$IMAGE_REF" ]; then
echo "❌ Fehler: Kein Image in $COMPOSE_FILE gefunden."
exit 1
fi
echo "⬇️ Lade Container-Image..."
if docker compose -f "$COMPOSE_FILE" pull; then
echo "✅ Image-Pull erfolgreich."
else
echo "⚠️ Image-Pull fehlgeschlagen. Prüfe lokales Image: $IMAGE_REF"
if docker image inspect "$IMAGE_REF" >/dev/null 2>&1; then
echo "✅ Lokales Image gefunden. Deployment läuft mit lokalem Image weiter."
else
echo "❌ Weder Registry-Pull erfolgreich noch lokales Image vorhanden: $IMAGE_REF"
echo " Bitte Registry-Zugriff prüfen oder ein lokales Image mit genau diesem Tag bereitstellen."
exit 1
fi
fi
echo "🚀 Starte Container..."
docker compose -f "$COMPOSE_FILE" up -d
echo "✅ Deployment abgeschlossen."
docker compose -f "$COMPOSE_FILE" ps

28
docker-compose.deploy.yml Normal file
View File

@@ -0,0 +1,28 @@
networks:
proxy:
name: proxy
external: true
services:
pdf-to-ics-web:
image: ${PDF_TO_ICS_IMAGE:-git.file-archive.de/webfarben/pdf_to_ics:v1.2.2}
container_name: pdf-to-ics-web
restart: unless-stopped
ports:
- "8000:8000"
networks:
- proxy
environment:
- TZ=Europe/Berlin
# Optional aktivieren für App-Login:
- WEB_AUTH_USER=${WEB_AUTH_USER:-}
- WEB_AUTH_PASSWORD=${WEB_AUTH_PASSWORD:-}
# Optional Matomo-Tracking:
- MATOMO_URL=${MATOMO_URL:-}
- MATOMO_SITE_ID=${MATOMO_SITE_ID:-}
healthcheck:
test: ["CMD", "python3", "-c", "import http.client,sys;c=http.client.HTTPConnection('127.0.0.1',8000,timeout=5);c.request('GET','/');r=c.getresponse();sys.exit(0 if r.status in (200,401) else 1)"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s

739
gui.py
View File

@@ -1,739 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GUI für PDF zu ICS Konverter
Grafische Benutzeroberfläche mit Tkinter
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from pathlib import Path
import threading
import re
import json
import webbrowser
from pdf_to_ics import extract_dienstplan_data, create_ics_from_dienstplan
from update_checker import check_for_updates, get_current_version
# Versuche tkinterdnd2 zu importieren (optional für besseres Drag & Drop)
try:
from tkinterdnd2 import DND_FILES, TkinterDnD
HAS_TKINTERDND = True
except ImportError:
HAS_TKINTERDND = False
# Konfigurationsdatei
CONFIG_FILE = Path.home() / '.pdf_to_ics_config.json'
class PDFtoICSGUI:
def __init__(self, root):
self.root = root
self.root.title("PDF zu ICS Konverter - Dienstplan Importer")
self.root.geometry("800x600")
self.root.minsize(700, 500)
# Lade gespeicherte Einstellungen
self.config = self.load_config()
# Variablen
self.pdf_files = []
# Erstelle Menüleiste
self.create_menu()
# Nutze letztes Ausgabeverzeichnis oder Standard
default_dir = self.config.get('last_output_dir', None)
if not default_dir or not Path(default_dir).exists():
default_dir = Path.cwd()
if str(default_dir).split('/')[-1].startswith('.'):
default_dir = Path.home()
self.output_dir = tk.StringVar(value=str(default_dir))
# Letztes PDF-Verzeichnis merken
self.last_pdf_dir = self.config.get('last_pdf_dir', str(Path.home()))
# Exportoptionen
self.exclude_rest = tk.BooleanVar(value=self.config.get('exclude_rest', False))
# UI erstellen
self.create_widgets()
# Drag & Drop einrichten
self.setup_drag_and_drop()
# Update-Prüfung im Hintergrund starten
update_thread = threading.Thread(target=self.check_for_updates_background, daemon=True)
update_thread.start()
# Speichere Konfiguration beim Schließen
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
def create_widgets(self):
"""Erstelle die UI-Komponenten"""
# Header
header_frame = tk.Frame(self.root, bg="#2c3e50", height=80)
header_frame.pack(fill=tk.X)
header_frame.pack_propagate(False)
title_label = tk.Label(
header_frame,
text="📅 PDF zu ICS Konverter",
font=("Arial", 20, "bold"),
bg="#2c3e50",
fg="white"
)
title_label.pack(pady=20)
# Main Content Frame
content_frame = tk.Frame(self.root, padx=20, pady=20)
content_frame.pack(fill=tk.BOTH, expand=True)
# PDF-Dateien Bereich
pdf_label = tk.Label(
content_frame,
text="PDF-Dateien:",
font=("Arial", 12, "bold")
)
pdf_label.pack(anchor=tk.W, pady=(0, 5))
# Listbox mit Scrollbar für PDFs
list_frame = tk.Frame(content_frame)
list_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
scrollbar = tk.Scrollbar(list_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.pdf_listbox = tk.Listbox(
list_frame,
selectmode=tk.EXTENDED,
yscrollcommand=scrollbar.set,
font=("Arial", 10)
)
self.pdf_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.config(command=self.pdf_listbox.yview)
# Buttons für PDF-Verwaltung
button_frame = tk.Frame(content_frame)
button_frame.pack(fill=tk.X, pady=(0, 10))
add_btn = tk.Button(
button_frame,
text=" PDF hinzufügen",
command=self.add_pdf_files,
bg="#3498db",
fg="white",
font=("Arial", 10, "bold"),
padx=20,
pady=8,
cursor="hand2"
)
add_btn.pack(side=tk.LEFT, padx=(0, 5))
remove_btn = tk.Button(
button_frame,
text=" Entfernen",
command=self.remove_selected_pdfs,
bg="#e74c3c",
fg="white",
font=("Arial", 10, "bold"),
padx=20,
pady=8,
cursor="hand2"
)
remove_btn.pack(side=tk.LEFT, padx=(0, 5))
clear_btn = tk.Button(
button_frame,
text="🗑️ Alle entfernen",
command=self.clear_all_pdfs,
bg="#95a5a6",
fg="white",
font=("Arial", 10, "bold"),
padx=20,
pady=8,
cursor="hand2"
)
clear_btn.pack(side=tk.LEFT)
# Konvertieren Button (rechts in der Zeile)
self.convert_btn = tk.Button(
button_frame,
text="📄 ICS Datei erstellen",
command=self.convert_pdfs,
bg="#27ae60",
fg="white",
font=("Arial", 10, "bold"),
padx=20,
pady=8,
cursor="hand2"
)
self.convert_btn.pack(side=tk.RIGHT)
# Ausgabe-Verzeichnis
output_frame = tk.Frame(content_frame)
output_frame.pack(fill=tk.X, pady=(10, 10))
output_label = tk.Label(
output_frame,
text="Ausgabe-Verzeichnis:",
font=("Arial", 10)
)
output_label.pack(side=tk.LEFT, padx=(0, 5))
output_entry = tk.Entry(
output_frame,
textvariable=self.output_dir,
font=("Arial", 10),
state="readonly"
)
output_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
browse_btn = tk.Button(
output_frame,
text="📁 Durchsuchen",
command=self.browse_output_dir,
bg="#16a085",
fg="white",
font=("Arial", 10),
padx=15,
pady=5,
cursor="hand2"
)
browse_btn.pack(side=tk.LEFT)
# Exportoptionen
options_frame = tk.Frame(content_frame)
options_frame.pack(fill=tk.X, pady=(10, 5))
exclude_rest_check = tk.Checkbutton(
options_frame,
text="🧘 Ruhetage ausschließen - (Ruhe, R56, R36, vRWF48, RWE, vR48)",
variable=self.exclude_rest,
font=("Arial", 10)
)
exclude_rest_check.pack(anchor=tk.W)
# Log-Bereich
log_label = tk.Label(
content_frame,
text="Status:",
font=("Arial", 10, "bold")
)
log_label.pack(anchor=tk.W, pady=(10, 5))
self.log_text = scrolledtext.ScrolledText(
content_frame,
height=8,
font=("Consolas", 9),
bg="#f8f9fa",
state=tk.DISABLED
)
self.log_text.pack(fill=tk.BOTH, expand=True)
# Fortschrittsbalken
self.progress = ttk.Progressbar(
content_frame,
mode='determinate',
length=300
)
drag_info = " (Drag & Drop unterstützt)" if HAS_TKINTERDND else " (Tipp: Installiere tkinterdnd2 für Drag & Drop)"
self.log(f"Bereit. Fügen Sie PDF-Dateien hinzu um zu starten.{drag_info}")
def load_config(self):
"""Lade gespeicherte Konfiguration"""
try:
if CONFIG_FILE.exists():
with open(CONFIG_FILE, 'r') as f:
return json.load(f)
except Exception as e:
print(f"Warnung: Konfiguration konnte nicht geladen werden: {e}")
return {}
def create_menu(self):
"""Erstelle die Menüleiste"""
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
# Hilfe-Menü
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Hilfe", menu=help_menu)
help_menu.add_command(label="PDF-Export auf Android (iPD)", command=self.show_android_export_guide)
help_menu.add_separator()
help_menu.add_command(label="Über dieses Programm", command=self.show_about_dialog)
help_menu.add_separator()
help_menu.add_command(label="Beenden", command=self.on_closing)
def show_android_export_guide(self):
"""Zeige Anleitung für PDF-Export aus Android App (iPD)"""
guide_window = tk.Toplevel(self.root)
guide_window.title("PDF-Export auf Android (iPD)")
guide_window.geometry("550x550")
guide_window.resizable(False, False)
# Zentriere das Fenster
guide_window.transient(self.root)
guide_window.grab_set()
# Header
header = tk.Label(
guide_window,
text="PDF-Export aus iPD",
font=("Arial", 16, "bold"),
bg="#2c3e50",
fg="white",
pady=15
)
header.pack(fill=tk.X)
# Content Frame
content = tk.Frame(guide_window, padx=20, pady=20)
content.pack(fill=tk.BOTH, expand=True)
# Anleitung-Text
guide_text = tk.Text(
content,
height=20,
font=("Courier", 9),
fg="#34495e",
wrap=tk.WORD,
relief=tk.FLAT,
bg="#f8f9fa"
)
guide_content = """1. Öffne die iPD App auf deinem Android-Gerät
2. Öffne einen Dienstplan
3. Wähle den gewünschten Monat aus
4. Tippe auf das PDF-Symbol
(rechts oben, links neben dem 3-Punkte-Menü)
5. Tippe auf "Datei herunterladen"
(rechts oben, neben Drucker-Button)
6. Wähle "Im Arbeitsprofil speichern"
7. Sende die PDF-Datei als E-Mail-Anhang
an deine private E-Mailadresse
8. Transferiere die PDF-Datei auf deinen Computer
9. Öffne diese Anwendung und füge die PDF ein
10. Klicke "ICS Datei erstellen"
11. Importiere die ICS-Datei in deinen Kalender
✓ Fertig!"""
guide_text.insert(tk.END, guide_content)
guide_text.config(state=tk.DISABLED)
guide_text.pack(fill=tk.BOTH, expand=True)
# Button-Frame
button_frame = tk.Frame(content)
button_frame.pack(fill=tk.X, pady=(15, 0))
# Online-Link Button
online_btn = tk.Button(
button_frame,
text="📖 Detaillierte Anleitung online",
command=lambda: webbrowser.open("https://git.file-archive.de/webfarben/pdf_to_ics"),
bg="#3498db",
fg="white",
font=("Arial", 10, "bold"),
padx=20,
pady=10,
cursor="hand2"
)
online_btn.pack(side=tk.LEFT, padx=(0, 10))
# Close Button
close_btn = tk.Button(
button_frame,
text="Schließen",
command=guide_window.destroy,
bg="#95a5a6",
fg="white",
font=("Arial", 10, "bold"),
padx=20,
pady=10,
cursor="hand2"
)
close_btn.pack(side=tk.LEFT)
def show_about_dialog(self):
"""Zeige About-Dialog mit Programminformationen"""
about_window = tk.Toplevel(self.root)
about_window.title("Über dieses Programm")
about_window.geometry("500x400")
about_window.resizable(False, False)
# Zentriere das Fenster
about_window.transient(self.root)
about_window.grab_set()
# Header
header = tk.Label(
about_window,
text="PDF zu ICS Konverter",
font=("Arial", 16, "bold"),
bg="#2c3e50",
fg="white",
pady=15
)
header.pack(fill=tk.X)
# Content Frame
content = tk.Frame(about_window, padx=20, pady=20)
content.pack(fill=tk.BOTH, expand=False)
# Version
version = get_current_version()
version_label = tk.Label(
content,
text=f"Version {version}",
font=("Arial", 11, "bold"),
fg="#2c3e50"
)
version_label.pack(anchor=tk.W, pady=(0, 15))
# Info-Texte
info_texts = [
("Firma:", "Webfarben"),
("Programmierer:", "Sebastian Köhler"),
("Kontakt:", "kontakt@webfarben.de"),
]
for label, value in info_texts:
frame = tk.Frame(content)
frame.pack(anchor=tk.W, pady=3, fill=tk.X)
label_widget = tk.Label(
frame,
text=label,
font=("Arial", 10, "bold"),
width=15,
anchor=tk.W
)
label_widget.pack(side=tk.LEFT)
value_widget = tk.Label(
frame,
text=value,
font=("Arial", 10),
fg="#34495e"
)
value_widget.pack(side=tk.LEFT, padx=(5, 0))
# Git Repository mit Link
repo_frame = tk.Frame(content)
repo_frame.pack(anchor=tk.W, pady=(15, 0), fill=tk.X)
repo_label = tk.Label(
repo_frame,
text="Repository:",
font=("Arial", 10, "bold"),
width=15,
anchor=tk.W
)
repo_label.pack(side=tk.LEFT)
repo_url = "https://git.file-archive.de/webfarben/pdf_to_ics.git"
repo_link = tk.Label(
repo_frame,
text=repo_url,
font=("Arial", 10, "underline"),
fg="#3498db",
cursor="hand2"
)
repo_link.pack(side=tk.LEFT, padx=(5, 0))
repo_link.bind("<Button-1>", lambda e: webbrowser.open(repo_url))
# Beschreibung
desc_frame = tk.Frame(content)
desc_frame.pack(anchor=tk.W, pady=(20, 0), fill=tk.BOTH, expand=False)
desc_text = tk.Text(
desc_frame,
height=4,
font=("Arial", 9),
fg="#34495e",
wrap=tk.WORD,
relief=tk.FLAT,
bg=about_window.cget("bg")
)
desc_text.insert(tk.END,
"Ein Programm zur Konvertierung von Dienstplan-PDFs "
"zu ICS-Kalenderdateien für einfaches Importieren "
"in Kalenderprogramme.")
desc_text.config(state=tk.DISABLED)
desc_text.pack(fill=tk.BOTH, expand=False)
# Close Button
close_btn = tk.Button(
about_window,
text="Schließen",
command=about_window.destroy,
bg="#3498db",
fg="white",
font=("Arial", 10, "bold"),
padx=50,
pady=12,
cursor="hand2"
)
close_btn.pack(pady=(10, 15), fill=tk.X, padx=20)
def save_config(self):
"""Speichere Konfiguration"""
try:
config = {
'last_output_dir': self.output_dir.get(),
'last_pdf_dir': self.last_pdf_dir,
'exclude_rest': self.exclude_rest.get()
}
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2)
except Exception as e:
print(f"Warnung: Konfiguration konnte nicht gespeichert werden: {e}")
def on_closing(self):
"""Handle für Fenster schließen"""
self.save_config()
self.root.destroy()
def setup_drag_and_drop(self):
"""Richte Drag & Drop ein"""
if HAS_TKINTERDND:
# Verwende tkinterdnd2 wenn verfügbar
self.pdf_listbox.drop_target_register(DND_FILES)
self.pdf_listbox.dnd_bind('<<Drop>>', self.drop_files)
else:
# Fallback: Native Tkinter Drag & Drop (funktioniert auf Unix)
try:
self.pdf_listbox.drop_target_register('DND_Files')
self.pdf_listbox.dnd_bind('<<Drop>>', self.drop_files)
except:
# Wenn auch das nicht funktioniert, zeige Hilfstext
pass
def drop_files(self, event):
"""Handle für Drag & Drop Events"""
files = []
if HAS_TKINTERDND:
# Parse tkinterdnd2 format
files_str = event.data
# Entferne geschweifte Klammern und splitte
files_str = files_str.strip('{}')
files = re.findall(r'[^\s{}]+(?:\s+[^\s{}]+)*', files_str)
else:
# Fallback parsing
if hasattr(event, 'data'):
files_str = event.data.strip('{}')
files = [f.strip() for f in files_str.split()]
# Füge nur PDF-Dateien hinzu
pdf_count = 0
for file_path in files:
file_path = file_path.strip()
if file_path.lower().endswith('.pdf'):
if file_path not in self.pdf_files:
self.pdf_files.append(file_path)
self.pdf_listbox.insert(tk.END, Path(file_path).name)
pdf_count += 1
if pdf_count > 0:
self.log(f"{pdf_count} PDF-Datei(en) per Drag & Drop hinzugefügt")
elif files:
self.log("⚠ Nur PDF-Dateien können hinzugefügt werden")
return 'break'
def log(self, message):
"""Füge eine Nachricht zum Log hinzu"""
self.log_text.config(state=tk.NORMAL)
self.log_text.insert(tk.END, message + "\n")
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
def add_pdf_files(self):
"""Öffne Datei-Dialog zum Hinzufügen von PDFs"""
# Starte im letzten PDF-Verzeichnis
initial_dir = self.last_pdf_dir
if initial_dir.startswith('.') or not Path(initial_dir).exists():
initial_dir = str(Path.home())
files = filedialog.askopenfilenames(
title="PDF-Dateien auswählen",
filetypes=[("PDF Dateien", "*.pdf"), ("Alle Dateien", "*.*")],
initialdir=initial_dir
)
for file in files:
if file not in self.pdf_files:
self.pdf_files.append(file)
self.pdf_listbox.insert(tk.END, Path(file).name)
if files:
# Merke Verzeichnis der ersten ausgewählten Datei
self.last_pdf_dir = str(Path(files[0]).parent)
self.log(f"{len(files)} PDF-Datei(en) hinzugefügt")
def remove_selected_pdfs(self):
"""Entferne ausgewählte PDFs aus der Liste"""
selected = self.pdf_listbox.curselection()
# Rückwärts durchlaufen, um Indexprobleme zu vermeiden
for index in reversed(selected):
self.pdf_listbox.delete(index)
del self.pdf_files[index]
if selected:
self.log(f"{len(selected)} PDF-Datei(en) entfernt")
def clear_all_pdfs(self):
"""Entferne alle PDFs aus der Liste"""
count = len(self.pdf_files)
self.pdf_listbox.delete(0, tk.END)
self.pdf_files.clear()
if count > 0:
self.log(f"✓ Alle {count} PDF-Datei(en) entfernt")
def browse_output_dir(self):
"""Öffne Dialog zur Auswahl des Ausgabe-Verzeichnisses"""
# Verhindere Start in versteckten Verzeichnissen
initial_dir = self.output_dir.get()
if initial_dir.startswith('.') or '/.venv' in initial_dir or '/__pycache__' in initial_dir:
initial_dir = str(Path.home())
directory = filedialog.askdirectory(
title="Ausgabe-Verzeichnis auswählen",
initialdir=initial_dir,
mustexist=True
)
if directory:
self.output_dir.set(directory)
self.save_config() # Sofort speichern
self.log(f"✓ Ausgabe-Verzeichnis: {directory}")
def convert_pdfs(self):
"""Konvertiere alle PDFs zu ICS"""
if not self.pdf_files:
messagebox.showwarning(
"Keine PDFs",
"Bitte fügen Sie mindestens eine PDF-Datei hinzu."
)
return
# Starte Konvertierung in separatem Thread
thread = threading.Thread(target=self._convert_worker, daemon=True)
thread.start()
def _convert_worker(self):
"""Worker-Thread für Konvertierung"""
self.progress['maximum'] = len(self.pdf_files)
self.progress['value'] = 0
output_dir = Path(self.output_dir.get())
success_count = 0
self.log("\n" + "="*50)
self.log("🔄 Starte Konvertierung...")
self.log("="*50)
for i, pdf_path in enumerate(self.pdf_files, 1):
try:
self.log(f"\n[{i}/{len(self.pdf_files)}] Verarbeite: {Path(pdf_path).name}")
# Extrahiere Daten
dienstplan = extract_dienstplan_data(pdf_path)
# Zeige Informationen
self.log(f" ├─ Name: {dienstplan['vorname']} {dienstplan['name']}")
self.log(f" ├─ Personalnummer: {dienstplan['personalnummer']}")
self.log(f" ├─ Betriebshof: {dienstplan['betriebshof']}")
self.log(f" └─ Events gefunden: {len(dienstplan['events'])}")
if not dienstplan['events']:
self.log(" ⚠️ Warnung: Keine Events gefunden!")
continue
# Erstelle ICS-Datei
ics_filename = Path(pdf_path).stem + '.ics'
ics_path = output_dir / ics_filename
create_ics_from_dienstplan(dienstplan, str(ics_path), exclude_rest=self.exclude_rest.get())
self.log(f" ✓ ICS erstellt: {ics_filename}")
success_count += 1
except Exception as e:
self.log(f" ✗ Fehler: {str(e)}")
finally:
self.progress['value'] = i
# Zusammenfassung
self.log("\n" + "="*50)
self.log(f"✅ Fertig! {success_count}/{len(self.pdf_files)} ICS-Dateien erstellt")
self.log("="*50 + "\n")
# Erfolgsmeldung
if success_count > 0:
self.root.after(0, lambda: messagebox.showinfo(
"Konvertierung abgeschlossen",
f"Es wurden {success_count} ICS-Datei(en) erfolgreich erstellt!\n\n"
f"Speicherort: {output_dir}"
))
def check_for_updates_background(self):
"""Prüfe auf Updates im Hintergrund"""
try:
update_available, new_version, download_url = check_for_updates()
if update_available:
# Zeige Update-Dialog auf dem Main-Thread
self.root.after(0, self.show_update_dialog, new_version, download_url)
except Exception as e:
# Stille Fehler ignorieren, damit GUI nicht beeinflusst wird
pass
def show_update_dialog(self, new_version, download_url):
"""Zeige Update-Dialog"""
current_version = get_current_version()
response = messagebox.showinfo(
"Update verfügbar",
f"Eine neue Version ist verfügbar!\n\n"
f"Aktuelle Version: v{current_version}\n"
f"Neue Version: v{new_version}\n\n"
f"Möchten Sie die neue Version herunterladen?"
)
if response == 'ok': # Dialog mit OK-Button
import webbrowser
webbrowser.open(download_url)
def main():
"""Hauptfunktion"""
if HAS_TKINTERDND:
# Verwende TkinterDnD root für besseres Drag & Drop
root = TkinterDnD.Tk()
else:
# Standard Tkinter
root = tk.Tk()
app = PDFtoICSGUI(root)
root.mainloop()
if __name__ == '__main__':
main()

View File

@@ -1,221 +0,0 @@
#!/bin/bash
###############################################################################
# PDF zu ICS Konverter - Installations-Script für Linux
# Installiert die Anwendung systemweit mit Desktop-Integration
###############################################################################
set -e # Beende bei Fehlern
# Farben für bessere Ausgabe
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Installation-Pfade
INSTALL_DIR="$HOME/.local/share/pdf-to-ics"
DESKTOP_FILE="$HOME/.local/share/applications/pdf-to-ics.desktop"
BIN_DIR="$HOME/.local/bin"
LAUNCHER="$BIN_DIR/pdf-to-ics"
echo -e "${BLUE}╔═══════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ PDF zu ICS Konverter - Installations-Assistent ║${NC}"
echo -e "${BLUE}╚═══════════════════════════════════════════════════════════╝${NC}"
echo ""
# Funktion für Fortschrittsanzeige
print_step() {
echo -e "${GREEN}${NC} $1"
}
print_warning() {
echo -e "${YELLOW}${NC} $1"
}
print_error() {
echo -e "${RED}${NC} $1"
}
print_success() {
echo -e "${GREEN}${NC} $1"
}
# Prüfe Python-Installation
print_step "Prüfe Python-Installation..."
if ! command -v python3 &> /dev/null; then
print_error "Python 3 ist nicht installiert!"
echo "Bitte installieren Sie Python 3:"
echo " sudo apt install python3 python3-pip python3-venv"
exit 1
fi
PYTHON_VERSION=$(python3 --version)
print_success "Python gefunden: $PYTHON_VERSION"
# Prüfe und installiere python3-venv wenn nötig
print_step "Prüfe venv-Installation..."
# Prüfe ob ensurepip verfügbar ist (wird für venv benötigt)
if ! python3 -c "import ensurepip" 2>/dev/null; then
print_warning "ensurepip ist nicht verfügbar. Installation von python3-venv erforderlich..."
# Erkenne Distribution
if [ -f /etc/debian_version ]; then
echo "Debian/Ubuntu erkannt. Installiere python3-venv..."
# Ermittle Python-Version für das richtige Paket
PYTHON_VERSION_NUM=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
VENV_PACKAGE="python${PYTHON_VERSION_NUM}-venv"
if command -v sudo &> /dev/null; then
sudo apt-get update && sudo apt-get install -y "$VENV_PACKAGE"
else
print_error "sudo nicht verfügbar. Bitte installieren Sie $VENV_PACKAGE manuell:"
echo " apt install $VENV_PACKAGE"
exit 1
fi
elif [ -f /etc/fedora-release ]; then
echo "Fedora erkannt. Python venv sollte bereits enthalten sein..."
print_error "Falls venv fehlt, installieren Sie bitte python3-devel"
exit 1
elif [ -f /etc/arch-release ]; then
echo "Arch Linux erkannt. Python venv sollte bereits enthalten sein..."
print_error "Falls venv fehlt, installieren Sie bitte python erneut"
exit 1
else
print_error "Distribution nicht erkannt. Bitte installieren Sie python3-venv manuell."
exit 1
fi
# Verifiziere Installation
if ! python3 -c "import ensurepip" 2>/dev/null; then
print_error "Installation von $VENV_PACKAGE fehlgeschlagen!"
exit 1
fi
print_success "venv installiert"
else
print_success "venv ist bereits installiert"
fi
# Prüfe und installiere Tkinter wenn nötig
print_step "Prüfe Tkinter-Installation..."
if ! python3 -c "import tkinter" 2>/dev/null; then
print_warning "Tkinter ist nicht installiert. Installation wird versucht..."
# Erkenne Distribution
if [ -f /etc/debian_version ]; then
echo "Debian/Ubuntu erkannt. Installiere python3-tk..."
if command -v sudo &> /dev/null; then
sudo apt-get update && sudo apt-get install -y python3-tk
else
print_error "sudo nicht verfügbar. Bitte installieren Sie python3-tk manuell:"
echo " apt-get install python3-tk"
exit 1
fi
elif [ -f /etc/fedora-release ]; then
echo "Fedora erkannt. Installiere python3-tkinter..."
sudo dnf install -y python3-tkinter
elif [ -f /etc/arch-release ]; then
echo "Arch Linux erkannt. Installiere tk..."
sudo pacman -S --noconfirm tk
else
print_warning "Distribution nicht erkannt. Bitte installieren Sie Tkinter manuell."
echo "Möchten Sie trotzdem fortfahren? (y/n)"
read -r response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
exit 1
fi
fi
print_success "Tkinter installiert"
else
print_success "Tkinter ist bereits installiert"
fi
# Erstelle Installationsverzeichnis
print_step "Erstelle Installationsverzeichnis..."
mkdir -p "$INSTALL_DIR"
mkdir -p "$BIN_DIR"
mkdir -p "$(dirname "$DESKTOP_FILE")"
# Kopiere Dateien
print_step "Kopiere Anwendungsdateien..."
cp -r *.py *.md *.sh *.cmd version.txt .gitignore "$INSTALL_DIR/" 2>/dev/null || true
print_success "Dateien kopiert nach $INSTALL_DIR"
# Erstelle Python Virtual Environment
print_step "Erstelle Python-Umgebung..."
cd "$INSTALL_DIR"
python3 -m venv .venv --upgrade-deps
print_success "Virtual Environment erstellt"
# Installiere Python-Abhängigkeiten
print_step "Installiere Python-Abhängigkeiten..."
.venv/bin/pip install -q --upgrade pip
.venv/bin/pip install -q pdfplumber icalendar pypdf2 pytz packaging
.venv/bin/pip install -q tkinterdnd2 2>/dev/null || print_warning "tkinterdnd2 optional nicht installiert (kein Problem)"
print_success "Abhängigkeiten installiert"
# Erstelle Launcher-Script
print_step "Erstelle Launcher-Script..."
cat > "$LAUNCHER" << 'EOF'
#!/bin/bash
# PDF zu ICS Konverter Launcher
INSTALL_DIR="$HOME/.local/share/pdf-to-ics"
cd "$INSTALL_DIR"
exec .venv/bin/python gui.py
EOF
chmod +x "$LAUNCHER"
print_success "Launcher erstellt: $LAUNCHER"
# Erstelle Desktop-Verknüpfung
print_step "Erstelle Desktop-Verknüpfung..."
cat > "$DESKTOP_FILE" << EOF
[Desktop Entry]
Version=1.0
Type=Application
Name=PDF zu ICS Konverter
Comment=Konvertiere Dienstplan-PDFs zu iCalendar-Dateien
Exec=$LAUNCHER
Icon=calendar
Terminal=false
Categories=Office;Utility;
Keywords=PDF;ICS;Kalender;Dienstplan;
StartupNotify=true
EOF
chmod +x "$DESKTOP_FILE"
print_success "Desktop-Verknüpfung erstellt"
# Desktop-Datenbank aktualisieren
if command -v update-desktop-database &> /dev/null; then
update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true
fi
# Prüfe ob ~/.local/bin im PATH ist
if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then
print_warning "~/.local/bin ist nicht im PATH!"
echo "Fügen Sie folgende Zeile zu ~/.bashrc oder ~/.zshrc hinzu:"
echo ""
echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
echo ""
fi
# Abschluss
echo ""
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Installation erfolgreich abgeschlossen! ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════╝${NC}"
echo ""
print_success "Die Anwendung wurde installiert!"
echo ""
echo "Sie können die Anwendung nun starten:"
echo ""
echo -e " 1. ${BLUE}Über das Anwendungsmenü${NC} (suchen Sie nach 'PDF zu ICS')"
echo -e " 2. ${BLUE}Über die Kommandozeile:${NC} pdf-to-ics"
echo ""
echo "Installation Details:"
echo " • Installationsverzeichnis: $INSTALL_DIR"
echo " • Launcher: $LAUNCHER"
echo " • Desktop-Verknüpfung: $DESKTOP_FILE"
echo ""
echo "Zum Deinstallieren führen Sie aus:"
echo " bash $INSTALL_DIR/uninstall.sh"
echo ""

201
menu.py
View File

@@ -1,201 +0,0 @@
#!/usr/bin/env python3
"""
Interaktives Menü für PDF zu ICS Konvertierung
Benutzerfreundliche Oberfläche zum Verarbeiten von Dienstplan-PDFs
"""
import os
import sys
from pathlib import Path
from pdf_to_ics import extract_dienstplan_data, create_ics_from_dienstplan
def print_header():
"""Zeige Programm-Header"""
print("\n" + "="*60)
print("PDF zu ICS Konverter - Dienstplan Importer".center(60))
print("="*60 + "\n")
def print_menu():
"""Zeige Hauptmenü"""
print("\nHauptmenü:")
print("1. PDF(s) konvertieren")
print("2. Verzeichnis durchsuchen")
print("3. Über dieses Programm")
print("4. Beenden")
print("-" * 40)
def list_pdf_files():
"""Liste alle PDF-Dateien im aktuellen Verzeichnis"""
pdf_files = list(Path('.').glob('*.pdf'))
return pdf_files
def convert_pdf(pdf_path):
"""Konvertiere eine einzelne PDF-Datei"""
try:
print(f"\n▶ Verarbeite: {pdf_path}")
# Extrahiere Daten
dienstplan = extract_dienstplan_data(str(pdf_path))
# Zeige Informationen
print(f" Name: {dienstplan['vorname']} {dienstplan['name']}")
print(f" Personalnummer: {dienstplan['personalnummer']}")
print(f" Betriebshof: {dienstplan['betriebshof']}")
print(f" Zeitraum: {dienstplan['monat_start']}")
print(f" Sollarbeitszeit: {dienstplan['sollarbeitszeit']}")
print(f" Events gefunden: {len(dienstplan['events'])}")
if not dienstplan['events']:
print(" ⚠ Warnung: Keine Events gefunden!")
return False
# Erstelle ICS-Datei
ics_path = pdf_path.with_suffix('.ics')
create_ics_from_dienstplan(dienstplan, str(ics_path))
print(f" ✓ ICS-Datei erstellt: {ics_path}")
print(f" ✓ Erfolg!\n")
return True
except Exception as e:
print(f" ✗ Fehler: {e}\n")
return False
def convert_multiple_pdfs():
"""Konvertiere mehrere PDF-Dateien"""
pdf_files = list_pdf_files()
if not pdf_files:
print("\n⚠ Keine PDF-Dateien im aktuellen Verzeichnis gefunden!")
return
print(f"\n{len(pdf_files)} PDF-Datei(en) gefunden:\n")
for i, pdf in enumerate(pdf_files, 1):
print(f"{i}. {pdf}")
print("\n" + "-" * 40)
success_count = 0
for pdf in pdf_files:
if convert_pdf(pdf):
success_count += 1
print(f"\n{'='*40}")
print(f"Zusammenfassung: {success_count}/{len(pdf_files)} ICS-Dateien erstellt")
print(f"{'='*40}\n")
def find_and_show_pdfs():
"""Durchsuche Verzeichnis und zeige PDFs"""
current_dir = Path('.')
print("\n📁 PDF-Dateien in diesem Verzeichnis:")
print("-" * 40)
pdf_files = list(current_dir.glob('*.pdf'))
if not pdf_files:
print("Keine PDF-Dateien gefunden.")
return
for i, pdf in enumerate(pdf_files, 1):
size = pdf.stat().st_size
size_mb = size / (1024 * 1024)
# Versuche Größe lesbar zu machen
if size_mb > 1:
size_str = f"{size_mb:.2f} MB"
else:
size_kb = size / 1024
size_str = f"{size_kb:.2f} KB"
print(f"{i}. {pdf.name:50} {size_str:>10}")
print("-" * 40)
print(f"\nGesamt: {len(pdf_files)} PDF-Datei(en)\n")
def show_about():
"""Zeige Informationen über das Programm"""
print("""
╔═══════════════════════════════════════════════════════════╗
║ PDF zu ICS Konverter - Dienstplan Importer ║
║ Version 1.0 ║
╚═══════════════════════════════════════════════════════════╝
BESCHREIBUNG:
Dieses Programm extrahiert Kalenderdaten aus Dienstplan-
PDFs und konvertiert sie in das iCalendar-Format (ICS).
FEATURES:
✓ Automatische Extraktion von Schichtdaten
✓ Erkennung von Zeitangaben und Nachtschichten
✓ Standard-konforme ICS-Datei-Erstellung
✓ Unterstützung für mehrere PDFs
VERWENDETE LIBRARIES:
• pdfplumber - PDF-Verarbeitung
• icalendar - ICS-Datei-Erstellung
• pytz - Zeitzonenverwaltung
IMPORT IN KALENDER:
Die erstellten ICS-Dateien können in folgende
Anwendungen importiert werden:
✓ Outlook
✓ Google Kalender
✓ Apple Kalender (macOS/iOS)
✓ Thunderbird
✓ LibreOffice Kalender
✓ und viele andere...
FÜR MEHR INFORMATIONEN:
Siehe README.md für ausführliche Dokumentation
""")
def main():
"""Hauptprogramm"""
print_header()
while True:
print_menu()
choice = input("Wählen Sie eine Option (1-4): ").strip()
if choice == '1':
convert_multiple_pdfs()
input("Drücken Sie Enter zum Fortfahren...")
elif choice == '2':
find_and_show_pdfs()
input("Drücken Sie Enter zum Fortfahren...")
elif choice == '3':
show_about()
input("Drücken Sie Enter zum Fortfahren...")
elif choice == '4':
print("\nAuf Wiedersehen! 👋\n")
sys.exit(0)
else:
print("\n✗ Ungültige Auswahl. Bitte versuchen Sie es erneut.")
os.system('clear' if os.name == 'posix' else 'cls')
print_header()
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print("\n\n✗ Programm von Benutzer unterbrochen.\n")
sys.exit(1)

View File

@@ -154,7 +154,7 @@ def parse_dienstplan_table(table, month_start_str):
return events
def create_ics_from_dienstplan(dienstplan, output_path=None, exclude_rest=False):
def create_ics_from_dienstplan(dienstplan, output_path=None, exclude_rest=False, exclude_vacation=False):
"""
Erstellt eine ICS-Datei aus den Dienstplan-Daten
@@ -162,6 +162,7 @@ def create_ics_from_dienstplan(dienstplan, output_path=None, exclude_rest=False)
dienstplan: Dictionary mit Dienstplan-Daten
output_path: Pfad für Output-Datei
exclude_rest: Wenn True, werden Ruhepausen nicht exportiert
exclude_vacation: Wenn True, wird Urlaub (060/0060) nicht exportiert
"""
# Erstelle Calendar
cal = Calendar()
@@ -182,12 +183,13 @@ def create_ics_from_dienstplan(dienstplan, output_path=None, exclude_rest=False)
continue
service_type = event_data['service']
normalized_service_type = service_type.lstrip('0') or '0'
event = Event()
# Titel - nur den Dienstart
# Spezielle Service-Typen mit aussagekräftigen Namen
if service_type == '0060':
if normalized_service_type == '60':
title = "Urlaub"
elif service_type in rest_types:
title = "Ruhe"
@@ -198,6 +200,10 @@ def create_ics_from_dienstplan(dienstplan, output_path=None, exclude_rest=False)
if exclude_rest and title == "Ruhe":
continue
# Überspringe Urlaub wenn gewünscht (060/0060)
if exclude_vacation and title == "Urlaub":
continue
event.add('summary', title)
# Beschreibung
@@ -268,6 +274,7 @@ Beispiele:
python3 pdf_to_ics.py # Konvertiere alle PDFs im aktuellen Verzeichnis
python3 pdf_to_ics.py --input ./pdfs --output ./ics # PDFs aus ./pdfs → ICS zu ./ics
python3 pdf_to_ics.py --input ./pdfs --exclude-rest # Schließe Ruhetage aus
python3 pdf_to_ics.py --input ./pdfs --exclude-vacation # Schließe Urlaub (060) aus
python3 pdf_to_ics.py file.pdf # Konvertiere einzelne Datei
"""
)
@@ -297,6 +304,12 @@ Beispiele:
help='Ruhetage ausschließen (Ruhe, R56, R36, vRWF48, RWE, vR48)'
)
parser.add_argument(
'-u', '--exclude-vacation',
action='store_true',
help='Urlaub ausschließen (060, 0060)'
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
@@ -336,6 +349,8 @@ Beispiele:
print(f"📄 PDF-Dateien gefunden: {len(pdf_files)}")
if args.exclude_rest:
print("🧘 Ruhetage werden ausgeschlossen")
if args.exclude_vacation:
print("🏖️ Urlaub (060) wird ausgeschlossen")
print()
success_count = 0
@@ -362,7 +377,12 @@ Beispiele:
# Erstelle ICS-Datei
ics_filename = pdf_file.stem + '.ics'
ics_path = output_dir / ics_filename
create_ics_from_dienstplan(dienstplan, str(ics_path), exclude_rest=args.exclude_rest)
create_ics_from_dienstplan(
dienstplan,
str(ics_path),
exclude_rest=args.exclude_rest,
exclude_vacation=args.exclude_vacation
)
print(f" ✓ ICS erstellt: {ics_path}")
success_count += 1

91
release.sh Executable file
View File

@@ -0,0 +1,91 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
IMAGE_REPO="git.file-archive.de/webfarben/pdf_to_ics"
SET_ENV=false
RUN_DEPLOY=false
usage() {
echo "Usage: ./release.sh <version-tag> [--set-env] [--deploy]"
echo ""
echo "Examples:"
echo " ./release.sh v1.2.3"
echo " ./release.sh v1.2.3 --set-env"
echo " ./release.sh v1.2.3 --set-env --deploy"
}
if [ $# -eq 1 ] && [[ "$1" == "-h" || "$1" == "--help" ]]; then
usage
exit 0
fi
if [ $# -lt 1 ]; then
usage
exit 1
fi
TAG="$1"
shift
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "❌ Fehler: Tag muss im Format vX.Y.Z sein (z. B. v1.2.3)."
exit 1
fi
while [ $# -gt 0 ]; do
case "$1" in
--set-env)
SET_ENV=true
;;
--deploy)
RUN_DEPLOY=true
;;
-h|--help)
usage
exit 0
;;
*)
echo "❌ Unbekannte Option: $1"
usage
exit 1
;;
esac
shift
done
if ! command -v docker >/dev/null 2>&1; then
echo "❌ Fehler: docker ist nicht installiert oder nicht im PATH."
exit 1
fi
IMAGE_REF="${IMAGE_REPO}:${TAG}"
echo "🏗️ Baue Image: $IMAGE_REF"
docker build -t "$IMAGE_REF" .
echo "⬆️ Pushe Image: $IMAGE_REF"
docker push "$IMAGE_REF"
if [ "$SET_ENV" = true ]; then
if [ -f ".env" ]; then
sed -i "s|^PDF_TO_ICS_IMAGE=.*|PDF_TO_ICS_IMAGE=$IMAGE_REF|" .env
echo "✅ .env auf $IMAGE_REF gesetzt."
else
echo " .env nicht gefunden übersprungen."
fi
fi
if [ "$RUN_DEPLOY" = true ]; then
if [ -x "./deploy.sh" ]; then
echo "🚀 Starte Deploy mit neuem Tag..."
./deploy.sh
else
echo "❌ Fehler: deploy.sh nicht gefunden oder nicht ausführbar."
exit 1
fi
fi
echo "✅ Release abgeschlossen: $IMAGE_REF"

View File

@@ -1,19 +0,0 @@
@echo off
REM PDF zu ICS Konverter - Windows Startskript
setlocal enabledelayedexpansion
REM Wechsel ins Skriptverzeichnis
cd /d "%~dp0"
REM Überprüfe, ob venv existiert
if not exist ".venv" (
echo Python-Umgebung wird eingerichtet...
python3 -m venv .venv
call .venv\Scripts\pip.exe install -q pdfplumber icalendar pypdf2 pytz
)
REM Starte das Menü
call .venv\Scripts\python.exe menu.py
pause

View File

@@ -1,78 +0,0 @@
#!/bin/bash
# PDF zu ICS Konverter - Startskript
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
# Finde Python-Executable
PYTHON_CMD=""
if command -v python3 &> /dev/null; then
PYTHON_CMD="python3"
elif command -v python &> /dev/null; then
PYTHON_CMD="python"
else
echo "❌ Fehler: Python nicht gefunden!"
echo "Bitte installieren Sie Python 3.6 oder höher."
exit 1
fi
echo "🐍 Nutze: $PYTHON_CMD"
# Erstelle venv wenn nicht vorhanden
if [ ! -d ".venv" ]; then
echo "📦 Python-Umgebung wird eingerichtet..."
$PYTHON_CMD -m venv .venv --upgrade-deps || {
echo "❌ venv konnte nicht erstellt werden"
exit 1
}
fi
# Nutze Python aus venv
PYTHON_VENV=".venv/bin/python"
# Überprüfe, ob Abhängigkeiten installiert sind
if ! $PYTHON_VENV -c "import pdfplumber" 2>/dev/null; then
echo "📚 Installiere Abhängigkeiten..."
# Nutze python -m pip statt pip direkt
if $PYTHON_VENV -m pip install -q pdfplumber icalendar pypdf2 pytz 2>/dev/null; then
echo "✓ Abhängigkeiten installiert"
else
echo "❌ Installation fehlgeschlagen"
echo "🔧 Versuche venv neu aufzubauen..."
rm -rf .venv
$PYTHON_CMD -m venv .venv --upgrade-deps || {
echo "❌ venv konnte nicht neu erstellt werden"
exit 1
}
$PYTHON_VENV -m pip install -q pdfplumber icalendar pypdf2 pytz || {
echo "❌ Abhängigkeiten konnten nicht installiert werden"
exit 1
}
echo "✓ venv neu erstellt"
fi
fi
# Starte das Skript
if [ -f "$PYTHON_VENV" ]; then
# Versuche zuerst das interaktive Menü, falls TTY verfügbar
if [ -t 0 ]; then
$PYTHON_VENV menu.py
else
# Sonst starte die direkte Konvertierung
echo ""
echo "🔄 Konvertiere PDF-Dateien..."
echo "-----------------------------------"
$PYTHON_VENV pdf_to_ics.py
echo "-----------------------------------"
echo ""
echo "✅ Fertig!"
fi
else
echo "❌ Fehler: Python-Umgebung ist beschädigt"
echo "📁 Bitte löschen Sie das .venv Verzeichnis und versuchen Sie erneut:"
echo " rm -rf .venv"
echo " ./start.sh"
exit 1
fi

View File

@@ -1,34 +0,0 @@
@echo off
REM PDF zu ICS Konverter - GUI Startskript (Windows)
setlocal enabledelayedexpansion
REM Wechsel ins Skriptverzeichnis
cd /d "%~dp0"
REM Überprüfe, ob venv existiert
if not exist ".venv" (
echo 📦 Python-Umgebung wird eingerichtet...
python3 -m venv .venv --upgrade-deps
if errorlevel 1 (
python -m venv .venv --upgrade-deps
)
)
REM Überprüfe, ob Abhängigkeiten installiert sind
.venv\Scripts\python.exe -c "import pdfplumber" 2>nul
if errorlevel 1 (
echo 📚 Installiere Abhängigkeiten...
call .venv\Scripts\python.exe -m pip install -q pdfplumber icalendar pypdf2 pytz
echo ✓ Abhängigkeiten installiert
)
REM Starte die GUI
echo 🎨 Starte GUI...
call .venv\Scripts\pythonw.exe gui.py
if errorlevel 1 (
echo.
echo ❌ Fehler beim Starten der GUI
pause
)

View File

@@ -1,72 +0,0 @@
#!/bin/bash
# PDF zu ICS Konverter - GUI Startskript
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
# Finde Python-Executable
PYTHON_CMD=""
if command -v python3 &> /dev/null; then
PYTHON_CMD="python3"
elif command -v python &> /dev/null; then
PYTHON_CMD="python"
else
echo "❌ Fehler: Python nicht gefunden!"
echo "Bitte installieren Sie Python 3.6 oder höher."
exit 1
fi
echo "🐍 Nutze: $PYTHON_CMD"
# Erstelle venv wenn nicht vorhanden
if [ ! -d ".venv" ]; then
echo "📦 Python-Umgebung wird eingerichtet..."
$PYTHON_CMD -m venv .venv --upgrade-deps || {
echo "❌ venv konnte nicht erstellt werden"
exit 1
}
fi
# Nutze Python aus venv
PYTHON_VENV=".venv/bin/python"
# Überprüfe, ob Abhängigkeiten installiert sind
if ! $PYTHON_VENV -c "import pdfplumber" 2>/dev/null; then
echo "📚 Installiere Abhängigkeiten..."
if $PYTHON_VENV -m pip install -q pdfplumber icalendar pypdf2 pytz 2>/dev/null; then
echo "✓ Abhängigkeiten installiert"
else
echo "❌ Installation fehlgeschlagen"
echo "🔧 Versuche venv neu aufzubauen..."
rm -rf .venv
$PYTHON_CMD -m venv .venv --upgrade-deps || {
echo "❌ venv konnte nicht neu erstellt werden"
exit 1
}
$PYTHON_VENV -m pip install -q pdfplumber icalendar pypdf2 pytz || {
echo "❌ Abhängigkeiten konnten nicht installiert werden"
exit 1
}
echo "✓ venv neu erstellt"
fi
fi
# Versuche tkinterdnd2 zu installieren (optional für Drag & Drop)
if ! $PYTHON_VENV -c "import tkinterdnd2" 2>/dev/null; then
echo "💡 Installiere tkinterdnd2 für Drag & Drop (optional)..."
$PYTHON_VENV -m pip install -q tkinterdnd2 2>/dev/null && echo "✓ Drag & Drop aktiviert" || echo " Drag & Drop nicht verfügbar (kein Problem)"
fi
# Starte die GUI
echo "🎨 Starte GUI..."
if [ -f "$PYTHON_VENV" ]; then
$PYTHON_VENV gui.py
else
echo "❌ Fehler: Python-Umgebung ist beschädigt"
echo "📁 Bitte löschen Sie das .venv Verzeichnis und versuchen Sie erneut:"
echo " rm -rf .venv"
echo " ./start_gui.sh"
exit 1
fi

View File

@@ -1,85 +0,0 @@
#!/bin/bash
###############################################################################
# PDF zu ICS Konverter - Deinstallations-Script
###############################################################################
set -e
# Farben
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
INSTALL_DIR="$HOME/.local/share/pdf-to-ics"
DESKTOP_FILE="$HOME/.local/share/applications/pdf-to-ics.desktop"
LAUNCHER="$HOME/.local/bin/pdf-to-ics"
CONFIG_FILE="$HOME/.pdf_to_ics_config.json"
echo -e "${YELLOW}╔═══════════════════════════════════════════════════════════╗${NC}"
echo -e "${YELLOW}║ PDF zu ICS Konverter - Deinstallations-Assistent ║${NC}"
echo -e "${YELLOW}╚═══════════════════════════════════════════════════════════╝${NC}"
echo ""
echo "Folgende Dateien und Verzeichnisse werden entfernt:"
echo "$INSTALL_DIR"
echo "$DESKTOP_FILE"
echo "$LAUNCHER"
echo ""
echo "Konfigurationsdatei behalten? (sie enthält Ihre letzten Verzeichnisse)"
echo "$CONFIG_FILE"
echo ""
echo -e "${RED}Möchten Sie fortfahren? (y/n)${NC}"
read -r response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
echo "Deinstallation abgebrochen."
exit 0
fi
# Lösche Dateien
echo ""
echo "Entferne Installationsdateien..."
if [ -d "$INSTALL_DIR" ]; then
rm -rf "$INSTALL_DIR"
echo -e "${GREEN}${NC} Installationsverzeichnis entfernt"
fi
if [ -f "$DESKTOP_FILE" ]; then
rm -f "$DESKTOP_FILE"
echo -e "${GREEN}${NC} Desktop-Verknüpfung entfernt"
fi
if [ -f "$LAUNCHER" ]; then
rm -f "$LAUNCHER"
echo -e "${GREEN}${NC} Launcher entfernt"
fi
# Konfigurationsdatei nur entfernen wenn explizit gewünscht
if [ -f "$CONFIG_FILE" ]; then
echo ""
echo -e "Konfigurationsdatei $CONFIG_FILE gefunden."
echo -e "${YELLOW}Auch entfernen? (y/n)${NC}"
read -r response
if [[ "$response" =~ ^[Yy]$ ]]; then
rm -f "$CONFIG_FILE"
echo -e "${GREEN}${NC} Konfigurationsdatei entfernt"
else
echo -e "${GREEN}${NC} Konfigurationsdatei behalten"
fi
fi
# Desktop-Datenbank aktualisieren
if command -v update-desktop-database &> /dev/null; then
update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true
fi
echo ""
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Deinstillation erfolgreich abgeschlossen! ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════╝${NC}"
echo ""
echo "PDF zu ICS Konverter wurde von Ihrem System entfernt."
echo ""

61
update.sh Executable file
View File

@@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
COMPOSE_FILE="docker-compose.deploy.yml"
if ! command -v docker >/dev/null 2>&1; then
echo "❌ Fehler: docker ist nicht installiert oder nicht im PATH."
exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
echo "❌ Fehler: docker compose ist nicht verfügbar."
exit 1
fi
if [ ! -f "$COMPOSE_FILE" ]; then
echo "❌ Fehler: $COMPOSE_FILE nicht gefunden."
exit 1
fi
if [ ! -f ".env" ]; then
if [ -f ".env.example" ]; then
cp .env.example .env
echo " .env wurde aus .env.example erstellt. Bitte Werte prüfen."
else
echo "❌ Fehler: .env fehlt und keine .env.example vorhanden."
exit 1
fi
fi
IMAGE_REF="$(docker compose -f "$COMPOSE_FILE" config | awk '/image:/{print $2; exit}')"
if [ -z "$IMAGE_REF" ]; then
echo "❌ Fehler: Kein Image in $COMPOSE_FILE gefunden."
exit 1
fi
echo "📥 Hole aktuelle Git-Änderungen..."
git pull --ff-only
echo "⬇️ Lade aktuelles Container-Image..."
if docker compose -f "$COMPOSE_FILE" pull; then
echo "✅ Image-Pull erfolgreich."
else
echo "⚠️ Image-Pull fehlgeschlagen. Prüfe lokales Image: $IMAGE_REF"
if docker image inspect "$IMAGE_REF" >/dev/null 2>&1; then
echo "✅ Lokales Image gefunden. Update läuft mit lokalem Image weiter."
else
echo "❌ Weder Registry-Pull erfolgreich noch lokales Image vorhanden: $IMAGE_REF"
echo " Bitte Registry-Zugriff prüfen oder ein lokales Image mit genau diesem Tag bereitstellen."
exit 1
fi
fi
echo "🚀 Starte/aktualisiere Container..."
docker compose -f "$COMPOSE_FILE" up -d
echo "✅ Update abgeschlossen."
docker compose -f "$COMPOSE_FILE" ps

View File

@@ -1,109 +0,0 @@
#!/usr/bin/env python3
"""
Update-Checker für PDF zu ICS Konverter
Prüft auf Updates über Gitea API
"""
import urllib.request
import json
from pathlib import Path
from packaging import version as pkg_version
# Gitea-Konfiguration
GITEA_URL = "https://git.file-archive.de"
REPO_OWNER = "webfarben"
REPO_NAME = "pdf_to_ics"
def get_current_version():
"""Lese aktuelle lokal gespeicherte Version"""
try:
version_file = Path(__file__).parent / 'version.txt'
if version_file.exists():
with open(version_file, 'r') as f:
return f.read().strip()
except Exception as e:
print(f"Warnung: Version konnte nicht gelesen werden: {e}")
return "0.0.0"
def get_latest_version():
"""
Prüfe Latest Release auf Gitea
Returns:
Tupel (version_string, download_url) oder (None, None) bei Fehler
"""
try:
# Gitea API Endpoint für neueste Release
url = f"{GITEA_URL}/api/v1/repos/{REPO_OWNER}/{REPO_NAME}/releases/latest"
with urllib.request.urlopen(url, timeout=5) as response:
data = json.loads(response.read().decode())
# Extrahiere Version aus Tag (z.B. "v1.0.0" -> "1.0.0")
tag = data.get('tag_name', '').lstrip('v')
if not tag:
return None, None
# HTML-URL für Download (Download-Seite auf Gitea)
download_url = f"{GITEA_URL}/{REPO_OWNER}/{REPO_NAME}/releases/tag/{data.get('tag_name', '')}"
return tag, download_url
except urllib.error.URLError as e:
print(f"Warnung: Konnte Gitea nicht erreichen ({GITEA_URL}): {e}")
return None, None
except json.JSONDecodeError:
print("Warnung: Gitea Response konnte nicht geparst werden")
return None, None
except Exception as e:
print(f"Warnung: Update-Check fehlgeschlagen: {e}")
return None, None
def check_for_updates():
"""
Prüfe ob ein Update verfügbar ist
Returns:
Tupel (update_available, new_version, download_url)
- update_available: Bool
- new_version: Version String oder None
- download_url: URL zur Download-Seite oder None
"""
current = get_current_version()
latest, url = get_latest_version()
if latest is None:
# Bei Fehler kein Update-Dialog
return False, None, None
try:
# Vergleiche Versionen
if pkg_version.parse(latest) > pkg_version.parse(current):
return True, latest, url
except Exception as e:
print(f"Warnung: Versionenvergleich fehlgeschlagen: {e}")
return False, None, None
if __name__ == "__main__":
# Test
current = get_current_version()
print(f"Aktuelle Version: {current}")
print(f"Gitea-Server: {GITEA_URL}/{REPO_OWNER}/{REPO_NAME}\n")
latest, url = get_latest_version()
if latest:
print(f"Neueste Version auf Gitea: {latest}")
print(f"Download-URL: {url}")
update_available, new_version, download_url = check_for_updates()
if update_available:
print(f"✓ Update verfügbar: v{new_version}")
else:
print("✓ Sie verwenden bereits die neueste Version")
else:
print(f"⚠ Konnte Gitea nicht erreichen ({GITEA_URL})")

View File

@@ -1 +0,0 @@
1.1.0

0
web/__init__.py Normal file
View File

230
web/app.py Normal file
View File

@@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""
Web-MVP für PDF zu ICS Konverter
"""
import re
import sys
import tempfile
from pathlib import Path
import os
import secrets
from fastapi import Depends, FastAPI, File, Form, HTTPException, Request, UploadFile, status
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.background import BackgroundTask
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from pdf_to_ics import create_ics_from_dienstplan, extract_dienstplan_data
app = FastAPI(title="PDF zu ICS Web")
templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates"))
app.mount("/static", StaticFiles(directory=str(Path(__file__).parent / "static")), name="static")
security = HTTPBasic()
def get_matomo_context():
matomo_url = (os.getenv("MATOMO_URL") or "").strip().rstrip("/")
matomo_site_id = (os.getenv("MATOMO_SITE_ID") or "").strip()
if not matomo_url or not matomo_site_id:
return {
"matomo_url": None,
"matomo_site_id": None,
}
return {
"matomo_url": matomo_url,
"matomo_site_id": matomo_site_id,
}
def require_auth(credentials: HTTPBasicCredentials = Depends(security)):
expected_user = os.getenv("WEB_AUTH_USER")
expected_password = os.getenv("WEB_AUTH_PASSWORD")
if not expected_user or not expected_password:
return
valid_user = secrets.compare_digest(credentials.username, expected_user)
valid_password = secrets.compare_digest(credentials.password, expected_password)
if not (valid_user and valid_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Unauthorized",
headers={"WWW-Authenticate": "Basic"},
)
@app.get("/", response_class=HTMLResponse)
def landing(request: Request):
context = {
"request": request,
}
context.update(get_matomo_context())
return templates.TemplateResponse(
"landing.html",
context,
)
@app.get("/app", response_class=HTMLResponse)
def index(request: Request, _: None = Depends(require_auth)):
context = {
"request": request,
"error": None,
}
context.update(get_matomo_context())
return templates.TemplateResponse(
"index.html",
context,
)
@app.post("/preview")
async def preview_pdf(
file: UploadFile = File(...),
exclude_rest: bool = Form(False),
exclude_vacation: bool = Form(False),
_: None = Depends(require_auth),
):
filename = file.filename or "dienstplan.pdf"
if not filename.lower().endswith(".pdf"):
return JSONResponse(
{"error": "Bitte eine PDF-Datei hochladen."},
status_code=400,
)
data = await file.read()
if not data:
return JSONResponse(
{"error": "Die Datei ist leer."},
status_code=400,
)
with tempfile.TemporaryDirectory(prefix="pdf_to_ics_web_preview_") as temp_dir:
temp_dir_path = Path(temp_dir)
pdf_path = temp_dir_path / "upload.pdf"
pdf_path.write_bytes(data)
dienstplan = extract_dienstplan_data(str(pdf_path))
if not dienstplan.get("events"):
return JSONResponse(
{"error": "Keine Dienstplan-Events in der PDF gefunden."},
status_code=422,
)
rest_types = ['R56', 'R36', 'vRWF48', 'RWE', 'vR48']
events_preview = []
for event in dienstplan["events"]:
service_type = event["service"]
normalized_service_type = service_type.lstrip('0') or '0'
# Wende Filter an
if exclude_rest and (service_type in rest_types or normalized_service_type == "Ruhe"):
continue
if exclude_vacation and normalized_service_type == "60":
continue
events_preview.append({
"date": event["date"].strftime("%d.%m.%Y") if event["date"] else "",
"service": event["service"] or "",
"time": f"{event['start_time']}-{event['end_time']}" if event["start_time"] and event["end_time"] else "",
})
return JSONResponse({
"success": True,
"filename": filename,
"metadata": {
"name": f"{dienstplan.get('vorname', '')} {dienstplan.get('name', '')}".strip(),
"personalnummer": dienstplan.get("personalnummer", ""),
"betriebshof": dienstplan.get("betriebshof", ""),
"count": len(events_preview),
},
"events": events_preview,
})
@app.post("/convert")
async def convert_pdf(
request: Request,
file: UploadFile = File(...),
exclude_rest: bool = Form(False),
exclude_vacation: bool = Form(False),
_: None = Depends(require_auth),
):
filename = file.filename or "dienstplan.pdf"
if not filename.lower().endswith(".pdf"):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"error": "Bitte eine PDF-Datei hochladen.",
},
status_code=400,
)
data = await file.read()
if not data:
return templates.TemplateResponse(
"index.html",
{
"request": request,
"error": "Die Datei ist leer.",
},
status_code=400,
)
with tempfile.TemporaryDirectory(prefix="pdf_to_ics_web_") as temp_dir:
temp_dir_path = Path(temp_dir)
pdf_path = temp_dir_path / "upload.pdf"
pdf_path.write_bytes(data)
dienstplan = extract_dienstplan_data(str(pdf_path))
if not dienstplan.get("events"):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"error": "Keine Dienstplan-Events in der PDF gefunden.",
},
status_code=422,
)
safe_name = re.sub(r"[^A-Za-z0-9._-]", "_", Path(filename).stem)
ics_filename = f"{safe_name}.ics"
ics_path = temp_dir_path / ics_filename
create_ics_from_dienstplan(
dienstplan,
str(ics_path),
exclude_rest=exclude_rest,
exclude_vacation=exclude_vacation,
)
content = ics_path.read_bytes()
response_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".ics", prefix="pdf_to_ics_out_")
response_tmp.write(content)
response_tmp.close()
return FileResponse(
path=response_tmp.name,
media_type="text/calendar",
filename=ics_filename,
headers={"Cache-Control": "no-store"},
background=BackgroundTask(lambda path: os.remove(path) if os.path.exists(path) else None, response_tmp.name),
)
@app.get("/health")
def health(_: None = Depends(require_auth)):
return {"status": "ok"}

9
web/requirements-web.txt Normal file
View File

@@ -0,0 +1,9 @@
fastapi>=0.115.0
uvicorn>=0.30.0
jinja2>=3.1.0
python-multipart>=0.0.9
pdfplumber
icalendar
pytz
pypdf2
packaging

View File

BIN
web/static/images/iPD01.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
web/static/images/iPD02.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
web/static/images/iPD03.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

403
web/templates/index.html Normal file
View File

@@ -0,0 +1,403 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PDF zu ICS (Web)</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
background: #f5f7fb;
color: #1f2937;
}
.wrap {
max-width: 600px;
margin: 0 auto;
padding: 20px 14px 28px;
}
.card {
background: #ffffff;
border-radius: 14px;
padding: 18px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
}
h1 {
margin: 0 0 6px;
font-size: 1.3rem;
}
p {
margin: 0 0 16px;
color: #4b5563;
line-height: 1.4;
}
label {
display: block;
font-weight: 600;
margin: 12px 0 8px;
}
input[type=file] {
width: 100%;
padding: 10px;
border: 1px solid #d1d5db;
border-radius: 10px;
background: #fff;
}
.options {
margin: 12px 0;
display: grid;
gap: 8px;
}
.option {
display: flex;
gap: 10px;
align-items: center;
font-weight: 500;
color: #111827;
}
button {
width: 100%;
border: 0;
border-radius: 10px;
padding: 12px;
font-size: 1rem;
font-weight: 700;
background: #2563eb;
color: #fff;
margin-top: 6px;
cursor: pointer;
transition: background 0.2s;
}
button:hover { background: #1d4ed8; }
button:active { transform: translateY(1px); }
button:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.error {
margin: 0 0 12px;
padding: 10px;
border-radius: 10px;
background: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
}
.hint {
margin-top: 12px;
font-size: 0.9rem;
color: #6b7280;
}
.hidden { display: none; }
.preview-header {
margin: 0 0 12px;
padding: 12px;
background: #f0f9ff;
border-radius: 10px;
border-left: 3px solid #2563eb;
}
.preview-meta {
font-size: 0.9rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin: 8px 0 0;
}
.preview-meta-item {
color: #4b5563;
}
.preview-meta-label {
font-weight: 600;
color: #1f2937;
}
.preview-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
margin: 12px 0;
}
.preview-table thead {
background: #f3f4f6;
}
.preview-table th, .preview-table td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #e5e7eb;
}
.preview-table th {
font-weight: 600;
color: #374151;
}
.preview-table tbody tr:hover {
background: #fafafa;
}
.actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.actions button {
margin-top: 0;
}
.btn-secondary {
background: #6b7280;
}
.btn-secondary:hover {
background: #4b5563;
}
.spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid #e5e7eb;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
{% if matomo_url and matomo_site_id %}
<script>
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u = '{{ matomo_url }}/';
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', '{{ matomo_site_id }}']);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true;
g.src = u + 'matomo.js';
s.parentNode.insertBefore(g, s);
})();
</script>
{% endif %}
</head>
<body>
<main class="wrap">
<section class="card">
<h1>PDF zu ICS Konverter</h1>
<p>PDF hochladen, Vorschau prüfen und herunterladen.</p>
<div id="error" class="error hidden"></div>
<!-- UPLOAD-FORMULAR -->
<div id="step-init">
<label for="file">Dienstplan-PDF</label>
<input id="file" type="file" accept="application/pdf,.pdf" />
<div class="options">
<label class="option">
<input id="exclude_rest" type="checkbox" />
Ruhetage ausschließen
</label>
<label class="option">
<input id="exclude_vacation" type="checkbox" />
Urlaub ausschließen
</label>
</div>
<button type="button" onclick="uploadAndPreview()">
Vorschau laden
</button>
<div class="hint">Hinweis: Die Datei wird nur temporär verarbeitet.</div>
</div>
<!-- PREVIEW -->
<div id="step2" class="hidden">
<div class="preview-header">
<div style="font-weight: 600; font-size: 0.95rem;" id="preview-name"></div>
<div class="preview-meta">
<div class="preview-meta-item">
<div class="preview-meta-label">Personalnummer</div>
<div id="preview-personalnummer"></div>
</div>
<div class="preview-meta-item">
<div class="preview-meta-label">Betriebshof</div>
<div id="preview-betriebshof"></div>
</div>
</div>
<div style="margin-top: 8px; color: #4b5563;">
<strong id="preview-count"></strong> Ereignisse gefunden
</div>
</div>
<table class="preview-table">
<thead>
<tr>
<th>Datum</th>
<th>Dienstart</th>
<th>Zeit</th>
</tr>
</thead>
<tbody id="preview-events"></tbody>
</table>
<div class="actions">
<button type="button" class="btn-secondary" onclick="resetForm()">Erneut hochladen</button>
<button type="button" onclick="downloadICS()">ICS herunterladen</button>
</div>
</div>
</section>
</main>
<script>
function trackMatomoEvent(category, action, name) {
if (window._paq && Array.isArray(window._paq)) {
window._paq.push(['trackEvent', category, action, name]);
}
}
let currentFile = null;
let currentFilename = null;
async function uploadAndPreview() {
const fileInput = document.getElementById("file");
currentFile = fileInput.files[0];
currentFilename = currentFile?.name;
if (!currentFile) {
showError("Bitte eine PDF-Datei auswählen.");
return;
}
const btn = event.target;
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Lädt Vorschau...';
try {
const formData = new FormData();
formData.append("file", currentFile);
formData.append("exclude_rest", document.getElementById("exclude_rest").checked);
formData.append("exclude_vacation", document.getElementById("exclude_vacation").checked);
const response = await fetch("/preview", {
method: "POST",
body: formData,
});
if (!response.ok) {
const data = await response.json();
showError(data.error || "Fehler beim Hochladen");
btn.disabled = false;
btn.textContent = "Vorschau laden";
return;
}
const data = await response.json();
if (!data.success) {
showError(data.error || "Fehler beim Vorschau-Laden");
btn.disabled = false;
btn.textContent = "Vorschau laden";
return;
}
showPreview(data);
hide("step-init");
show("step2");
} catch (error) {
showError(`Fehler: ${error.message}`);
btn.disabled = false;
btn.textContent = "Vorschau laden";
}
}
function showPreview(data) {
const { metadata, events } = data;
document.getElementById("preview-name").textContent = metadata.name;
document.getElementById("preview-personalnummer").textContent = metadata.personalnummer;
document.getElementById("preview-betriebshof").textContent = metadata.betriebshof;
document.getElementById("preview-count").textContent = metadata.count;
const tbody = document.getElementById("preview-events");
tbody.innerHTML = "";
events.forEach((event) => {
const tr = document.createElement("tr");
tr.innerHTML = `<td>${event.date}</td><td>${event.service}</td><td>${event.time}</td>`;
tbody.appendChild(tr);
});
}
async function downloadICS() {
const btn = event.target;
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Download...';
try {
const formData = new FormData();
formData.append("file", currentFile);
formData.append("exclude_rest", document.getElementById("exclude_rest").checked);
formData.append("exclude_vacation", document.getElementById("exclude_vacation").checked);
const response = await fetch("/convert", {
method: "POST",
body: formData,
});
if (!response.ok) {
showError("Fehler beim Download");
btn.disabled = false;
btn.textContent = "ICS herunterladen";
return;
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = currentFilename.replace(/\.pdf$/i, ".ics");
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
a.remove();
trackMatomoEvent("pdf_to_ics", "download", "ics_export");
resetForm();
} catch (error) {
showError(`Fehler: ${error.message}`);
btn.disabled = false;
btn.textContent = "ICS herunterladen";
}
}
function resetForm() {
document.getElementById("file").value = "";
document.getElementById("exclude_rest").checked = false;
document.getElementById("exclude_vacation").checked = false;
currentFile = null;
currentFilename = null;
hide("step2");
show("step-init");
hide("error");
// Stelle sicher, dass der Button aktiv ist
const btn = document.querySelector("#step-init button");
if (btn) {
btn.disabled = false;
btn.textContent = "Vorschau laden";
}
}
function showError(message) {
const el = document.getElementById("error");
el.textContent = message;
show("error");
}
function show(id) {
document.getElementById(id).classList.remove("hidden");
}
function hide(id) {
document.getElementById(id).classList.add("hidden");
}
</script>
</body>
</html>

159
web/templates/landing.html Normal file
View File

@@ -0,0 +1,159 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PDF zu ICS Dienstplan einfach importieren</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
background: #f5f7fb;
color: #111827;
}
.wrap {
max-width: 760px;
margin: 0 auto;
padding: 20px 14px 28px;
}
.card {
background: #fff;
border-radius: 14px;
padding: 18px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
margin-bottom: 12px;
}
h1 {
margin: 0 0 8px;
font-size: 1.5rem;
}
h2 {
margin: 0 0 8px;
font-size: 1.05rem;
}
p, li {
color: #4b5563;
line-height: 1.45;
margin: 0 0 10px;
}
ul {
margin: 0;
padding-left: 18px;
}
.cta {
display: inline-block;
text-decoration: none;
border-radius: 10px;
padding: 11px 14px;
font-weight: 700;
background: #2563eb;
color: #fff;
}
.hint {
font-size: 0.9rem;
color: #6b7280;
margin-top: 8px;
}
.server-note {
font-size: 0.88rem;
color: #6b7280;
margin: 10px 2px 0;
}
.image-guide {
margin-top: 18px;
}
.image-grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
.image-item {
background: #fff;
border-radius: 14px;
padding: 10px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
}
.image-item img {
display: block;
width: 100%;
border-radius: 10px;
height: auto;
}
.image-caption {
margin: 8px 2px 2px;
font-size: 0.92rem;
color: #4b5563;
}
</style>
{% if matomo_url and matomo_site_id %}
<script>
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u = '{{ matomo_url }}/';
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', '{{ matomo_site_id }}']);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true;
g.src = u + 'matomo.js';
s.parentNode.insertBefore(g, s);
})();
</script>
{% endif %}
</head>
<body>
<main class="wrap">
<section class="card">
<h1>PDF zu ICS Konverter</h1>
<p>Diese Anwendung wandelt Dienstplan-PDFs in iCalendar-Dateien (.ics) um, damit Schichten schnell in Kalender-Apps übernommen werden können.</p>
<a class="cta" href="/app">Zur Anwendung</a>
<p class="hint">Die Konvertierung erfolgt serverseitig, Uploads werden nur temporär verarbeitet.</p>
</section>
<section class="card">
<h2>Warum sinnvoll?</h2>
<ul>
<li>Kein manuelles Eintragen von Diensten in den Kalender</li>
<li>Importierbar in gängige Kalender (Google, Outlook, Apple, Thunderbird)</li>
<li>Vorschau vor dem Download der ICS-Datei</li>
</ul>
</section>
<section class="card">
<h2>So funktioniert es</h2>
<ul>
<li>Dienstplan-PDF hochladen</li>
<li>Extrahierte Schichten in der Vorschau prüfen</li>
<li>ICS herunterladen und im Kalender importieren</li>
</ul>
</section>
<section class="image-guide" aria-label="PDF-Export in iPD">
<h2>PDF-Export in iPD</h2>
<p>Die folgenden Bilder zeigen, wie der Dienstplan in iPD als PDF exportiert wird.</p>
<p class="server-note">Hinweis: Da der Benutzer im Arbeitsprofile keine Daten exportieren kann, bleibt nur die Möglichkeit das PDF per Dienstmail an eine private Mailadresse zu senden und anschließend hier hochzuladen.</p>
<div class="image-grid">
<figure class="image-item">
<img src="{{ request.url_for('static', path='images/iPD01.jpg') }}" alt="iPD Schritt 1: Dienstplanansicht öffnen" loading="lazy">
<figcaption class="image-caption">Schritt 1: Dienstplanansicht in iPD öffnen.</figcaption>
</figure>
<figure class="image-item">
<img src="{{ request.url_for('static', path='images/iPD02.jpg') }}" alt="iPD Schritt 2: Export- oder Druckmenü auswählen" loading="lazy">
<figcaption class="image-caption">Schritt 2: Export- oder Druckfunktion auswählen.</figcaption>
</figure>
<figure class="image-item">
<img src="{{ request.url_for('static', path='images/iPD03.jpg') }}" alt="iPD Schritt 3: PDF-Export starten" loading="lazy">
<figcaption class="image-caption">Schritt 3: PDF-Export starten.</figcaption>
</figure>
<figure class="image-item">
<img src="{{ request.url_for('static', path='images/iPD03_1.jpg') }}" alt="iPD Schritt 4: PDF speichern" loading="lazy">
<figcaption class="image-caption">Schritt 4: PDF speichern und anschließend hier hochladen.</figcaption>
</figure>
</div>
</section>
</main>
</body>
</html>