diff --git a/.idea/misc.xml b/.idea/misc.xml
index 060d2c5..db8786c 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/zls_export_helper.py b/zls_export_helper.py
index e69de29..c41c31b 100644
--- a/zls_export_helper.py
+++ b/zls_export_helper.py
@@ -0,0 +1,390 @@
+# -*- coding: utf-8 -*-
+"""
+ZLS-Export Leitfaden für Loco-Soft
+GUI mit Schritt-Screenshots je Workflow, CSV-Header-Prüfung via JSON,
+Umbenennen und Kopieren auf den Desktop, danach zurück zum Start.
+
+Voraussetzungen (installieren in der venv):
+ pip install customtkinter pillow
+
+Optional für EXE:
+ pip install pyinstaller
+"""
+
+import os
+import sys
+import json
+import glob
+import shutil
+import datetime
+import traceback
+import tkinter.filedialog as fd
+import tkinter.messagebox as mb
+
+import customtkinter as ctk
+from PIL import Image, ImageTk
+
+
+APP_TITLE = "Das ist ein Leitfaden zu Exportieren der ZLS Liste an Loco-Soft"
+
+# Namen der Workflows -> Ordnernamen unter ./assets
+WORKFLOWS = [
+ ("Kundenadressen", "kundenadressen"),
+ ("Fahrzeugdaten", "fahrzeugdaten"),
+ ("Bestandsfahrzeuge", "bestandsfahrzeuge"),
+ ("Reifenlager", "reifenlager"),
+ ("Rechnungsablage", "rechnungsablage"),
+]
+
+# Standard-Schema (falls assets/schema.json fehlt)
+# Du kannst das hier anpassen oder eine eigene schema.json in ./assets ablegen.
+DEFAULT_SCHEMA = {
+ # Prüf-Logik: erstes zutreffendes Pattern gewinnt
+ # "pattern": kann ein Teil des Dateinamens sein (case-insensitive)
+ # "header": entweder String (exakte 1. Zeile) ODER Liste der Spalten (wird zu CSV-Header zusammengefügt)
+ "rules": [
+ {"pattern": "kunden", "header": ["KundenNr", "Name", "Straße", "PLZ", "Ort", "Telefon", "E-Mail"]},
+ {"pattern": "fahrzeug", "header": ["FahrzeugID", "Marke", "Modell", "FIN", "EZ", "KM"]},
+ {"pattern": "bestand", "header": ["BestandID", "Marke", "Modell", "Status", "Standort"]},
+ {"pattern": "reifen", "header": ["KundenNr", "SatzNr", "Dimension", "Profil", "Lagerplatz"]},
+ {"pattern": "rechnung", "header": ["RechnungsNr", "Datum", "KundenNr", "Betrag", "Steuer", "Gesamt"]},
+ ],
+ # Wenn True: Header-Vergleich ist case-insensitive und trimmt Leerzeichen
+ "normalize": True,
+ # CSV-Delimiter-Erwartung für Header-Zeile (nur für den Vergleich)
+ "delimiter": ","
+}
+
+
+def resource_path(*relative):
+ """Findet Pfade auch im PyInstaller-Bundle."""
+ if hasattr(sys, "_MEIPASS"):
+ base = sys._MEIPASS # PyInstaller temp folder
+ else:
+ base = os.path.abspath(os.path.dirname(__file__))
+ return os.path.join(base, *relative)
+
+
+def load_schema():
+ path = resource_path("assets", "schema.json")
+ if os.path.isfile(path):
+ try:
+ with open(path, "r", encoding="utf-8") as f:
+ return json.load(f)
+ except Exception:
+ print("WARN: Konnte assets/schema.json nicht laden, verwende DEFAULT_SCHEMA.", file=sys.stderr)
+ return DEFAULT_SCHEMA
+
+
+def list_screenshots(workflow_folder):
+ # akzeptierte Bildformate
+ exts = ("*.png", "*.jpg", "*.jpeg", "*.webp", "*.bmp")
+ files = []
+ for e in exts:
+ files.extend(glob.glob(os.path.join(workflow_folder, e)))
+ files.sort()
+ return files
+
+
+def format_timestamp(dt=None):
+ dt = dt or datetime.datetime.now()
+ # ddmmyyyy_hhmm
+ return dt.strftime("%d%m%Y_%H%M")
+
+
+def normalize_header(text):
+ # Entfernt BOM, trims, vereinheitlicht Leerzeichen
+ t = text.replace("\ufeff", "").strip()
+ # Einheitliche Kommata
+ t = ",".join([p.strip() for p in t.split(",")])
+ return t
+
+
+def header_from_list(cols, delimiter=","):
+ return delimiter.join([str(c).strip() for c in cols])
+
+
+def match_rule(filename, rules):
+ low = filename.lower()
+ for rule in rules:
+ if str(rule.get("pattern", "")).lower() in low:
+ return rule
+ return None
+
+
+def validate_csv_first_line(file_path, schema):
+ """
+ Prüft die erste Zeile gegen das Schema.
+ Rückgabe: (ok: bool, message: str)
+ """
+ try:
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
+ first_line = f.readline().rstrip("\n\r")
+ except Exception as e:
+ return False, f"Fehler beim Lesen: {e}"
+
+ rules = schema.get("rules", [])
+ delimiter = schema.get("delimiter", ",")
+ normalize = bool(schema.get("normalize", True))
+
+ rule = match_rule(os.path.basename(file_path), rules)
+ if not rule:
+ return False, "Keine passende Regel (pattern) im Schema gefunden."
+
+ expected = rule.get("header")
+ if isinstance(expected, list):
+ expected = header_from_list(expected, delimiter=delimiter)
+ expected = expected or ""
+
+ actual = first_line
+ if normalize:
+ expected = normalize_header(expected)
+ actual = normalize_header(actual)
+
+ ok = (actual == expected)
+ msg = "Header OK" if ok else f"Header NICHT OK\nErwartet: {expected}\nGefunden: {actual}"
+ return ok, msg
+
+
+def ensure_desktop_target():
+ desktop = os.path.join(os.path.expanduser("~"), "Desktop")
+ target = os.path.join(desktop, "ZLS_Export")
+ os.makedirs(target, exist_ok=True)
+ return target
+
+
+class WorkflowViewer(ctk.CTkFrame):
+ def __init__(self, master, title, folder, on_finished, *args, **kwargs):
+ super().__init__(master, *args, **kwargs)
+ self.master = master
+ self.title = title
+ self.folder = folder
+ self.on_finished = on_finished
+
+ self.schema = load_schema()
+ self.images = []
+ self.image_paths = list_screenshots(self.folder)
+ self.idx = 0
+
+ # UI
+ self.grid_columnconfigure(0, weight=1)
+ self.grid_rowconfigure(1, weight=1)
+
+ self.lbl_title = ctk.CTkLabel(self, text=title, font=ctk.CTkFont(size=22, weight="bold"))
+ self.lbl_title.grid(row=0, column=0, pady=(8, 4), padx=12)
+
+ self.canvas = ctk.CTkLabel(self, text="", fg_color="transparent")
+ self.canvas.grid(row=1, column=0, sticky="nsew", padx=12, pady=8)
+
+ # Navigationsleiste
+ nav = ctk.CTkFrame(self)
+ nav.grid(row=2, column=0, sticky="ew", padx=12, pady=(0, 8))
+ nav.grid_columnconfigure((0, 1, 2, 3), weight=1)
+
+ self.btn_prev = ctk.CTkButton(nav, text="◀ Zurück", command=self.prev_step)
+ self.btn_prev.grid(row=0, column=0, padx=6, pady=6, sticky="w")
+
+ self.lbl_pos = ctk.CTkLabel(nav, text="–/–")
+ self.lbl_pos.grid(row=0, column=1, padx=6, pady=6)
+
+ self.btn_next = ctk.CTkButton(nav, text="Weiter ▶", command=self.next_step)
+ self.btn_next.grid(row=0, column=2, padx=6, pady=6, sticky="e")
+
+ self.btn_abort = ctk.CTkButton(nav, text="Zum Start", fg_color="grey30", command=self.back_to_home)
+ self.btn_abort.grid(row=0, column=3, padx=6, pady=6, sticky="e")
+
+ # Bild-Klick (für "letzter Screenshot geklickt")
+ self.canvas.bind("", self.on_image_click)
+
+ self.load_images()
+ self.update_view()
+
+ def load_images(self):
+ self.images = []
+ max_w = 1100
+ max_h = 650
+
+ if not self.image_paths:
+ # Platzhalter
+ placeholder = Image.new("RGB", (900, 500), color=(245, 245, 245))
+ self.images.append(ImageTk.PhotoImage(placeholder))
+ self.image_paths = ["(Kein Screenshot gefunden)"]
+ else:
+ for p in self.image_paths:
+ try:
+ img = Image.open(p)
+ img.thumbnail((max_w, max_h), Image.LANCZOS)
+ self.images.append(ImageTk.PhotoImage(img))
+ except Exception:
+ # Platzhalter bei Fehler
+ ph = Image.new("RGB", (900, 500), color=(245, 245, 245))
+ self.images.append(ImageTk.PhotoImage(ph))
+
+ def update_view(self):
+ self.canvas.configure(image=self.images[self.idx])
+ pos = f"Schritt {self.idx+1} von {len(self.images)}"
+ # Bildunterschrift als Tooltip-Ersatz (Dateiname)
+ caption = os.path.basename(self.image_paths[self.idx])
+ self.lbl_pos.configure(text=f"{pos} — {caption}")
+ self.btn_prev.configure(state=("normal" if self.idx > 0 else "disabled"))
+ self.btn_next.configure(state=("normal" if self.idx < len(self.images)-1 else "disabled"))
+
+ def prev_step(self):
+ if self.idx > 0:
+ self.idx -= 1
+ self.update_view()
+
+ def next_step(self):
+ if self.idx < len(self.images) - 1:
+ self.idx += 1
+ self.update_view()
+
+ def on_image_click(self, _event):
+ # Wenn letzter Screenshot geklickt wird → Exportfrage
+ if self.idx == len(self.images) - 1:
+ go = mb.askyesno("Export starten?", "Möchtest du jetzt den Export prüfen, umbenennen und kopieren?")
+ if go:
+ self.run_export_flow()
+
+ def run_export_flow(self):
+ try:
+ source_dir = fd.askdirectory(title="Ordner mit exportierten CSV-Dateien wählen")
+ if not source_dir:
+ return
+
+ csv_files = sorted(glob.glob(os.path.join(source_dir, "*.csv")))
+ if not csv_files:
+ mb.showerror("Keine CSVs gefunden", "Im gewählten Ordner wurden keine .csv-Dateien gefunden.")
+ return
+
+ schema = self.schema
+ results = []
+ all_ok = True
+
+ # Prüfung & Umbenennen
+ timestamp = format_timestamp()
+ renamed_paths = []
+ for f in csv_files:
+ ok, msg = validate_csv_first_line(f, schema)
+ results.append((os.path.basename(f), ok, msg))
+ if not ok:
+ all_ok = False
+
+ # Meldung zu den Prüfergebnissen
+ summary_lines = []
+ for name, ok, msg in results:
+ status = "✅ OK" if ok else "❌ FEHLER"
+ summary_lines.append(f"{status} {name}\n{msg}")
+ mb.showinfo("Prüfergebnis", "\n\n".join(summary_lines))
+
+ if not all_ok:
+ proceed = mb.askyesno("Trotzdem fortfahren?",
+ "Nicht alle Dateien sind OK. Möchtest du trotzdem mit Umbenennen und Kopieren fortfahren?")
+ if not proceed:
+ return
+
+ # Umbenennen (im Quellordner) & Sammeln
+ temp_paths = []
+ for f in csv_files:
+ base = os.path.basename(f)
+ new_name = f"{timestamp}_{base}"
+ new_path = os.path.join(os.path.dirname(f), new_name)
+ # Umbenennen nur, wenn noch nicht bereits geprefixt
+ if not base.startswith(timestamp + "_"):
+ os.rename(f, new_path)
+ temp_paths.append(new_path)
+ else:
+ temp_paths.append(f) # war schon umbenannt
+ renamed_paths = temp_paths
+
+ # Zielordner auf dem Desktop
+ target_dir = ensure_desktop_target()
+ for p in renamed_paths:
+ shutil.copy2(p, target_dir)
+
+ mb.showinfo("Fertig", f"Dateien wurden nach:\n{target_dir}\nkopiert.")
+ except Exception as e:
+ traceback.print_exc()
+ mb.showerror("Fehler", f"Es ist ein Fehler aufgetreten:\n{e}")
+ finally:
+ # Zurück zum Start
+ self.on_finished()
+
+ def back_to_home(self):
+ self.on_finished()
+
+
+class App(ctk.CTk):
+ def __init__(self):
+ super().__init__()
+ ctk.set_appearance_mode("system") # "light" / "dark" / "system"
+ ctk.set_default_color_theme("blue")
+
+ self.title("ZLS → Loco-Soft Leitfaden")
+ self.geometry("1200x800")
+ self.minsize(1000, 700)
+
+ self.container = ctk.CTkFrame(self)
+ self.container.pack(fill="both", expand=True)
+
+ self.show_home()
+
+ def clear(self):
+ for w in self.container.winfo_children():
+ w.destroy()
+
+ def show_home(self):
+ self.clear()
+ frame = ctk.CTkFrame(self.container)
+ frame.pack(fill="both", expand=True, padx=16, pady=16)
+ frame.grid_columnconfigure((0, 1, 2), weight=1)
+
+ lbl = ctk.CTkLabel(frame, text=APP_TITLE, font=ctk.CTkFont(size=24, weight="bold"))
+ lbl.grid(row=0, column=0, columnspan=3, pady=(8, 16))
+
+ sub = ctk.CTkLabel(frame, text="Bitte einen Bereich wählen, um den Schritt-für-Schritt-Leitfaden zu öffnen.")
+ sub.grid(row=1, column=0, columnspan=3, pady=(0, 16))
+
+ # Buttons in einem Grid
+ r, c = 2, 0
+ for display_name, folder in WORKFLOWS:
+ btn = ctk.CTkButton(
+ frame,
+ height=64,
+ text=display_name,
+ command=lambda d=display_name, f=folder: self.open_workflow(d, f),
+ )
+ btn.grid(row=r, column=c, padx=10, pady=10, sticky="ew")
+
+ c += 1
+ if c > 2:
+ c = 0
+ r += 1
+
+ info = ctk.CTkLabel(
+ frame,
+ text=("Hinweis:\n"
+ "Lege deine Schritt-Screenshots in Unterordnern unter ./assets// ab.\n"
+ "Optionale CSV-Header-Vorgaben in ./assets/schema.json."),
+ justify="left",
+ )
+ info.grid(row=r+1, column=0, columnspan=3, pady=(20, 0))
+
+ def open_workflow(self, display_name, folder_name):
+ abs_folder = resource_path("assets", folder_name)
+ if not os.path.isdir(abs_folder):
+ os.makedirs(abs_folder, exist_ok=True)
+
+ self.clear()
+ viewer = WorkflowViewer(
+ master=self.container,
+ title=display_name,
+ folder=abs_folder,
+ on_finished=self.show_home
+ )
+ viewer.pack(fill="both", expand=True, padx=8, pady=8)
+
+
+if __name__ == "__main__":
+ app = App()
+ app.mainloop()