From 2eef2c105298c6abf99b2f392735226447e8daf7 Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 21 May 2026 23:25:47 +0200 Subject: [PATCH] =?UTF-8?q?add:=20schau=20regelm=C3=A4=C3=9Fig=20nach=20ne?= =?UTF-8?q?uerem=20Excel=20file=20auf=20Netzlaufwerk=20oder=20zeige=20Warn?= =?UTF-8?q?ung=20wenn=20nicht=20erreichbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gui.py | 218 +++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 197 insertions(+), 21 deletions(-) diff --git a/gui.py b/gui.py index af9495a..569c9f8 100644 --- a/gui.py +++ b/gui.py @@ -107,6 +107,11 @@ def _count_output_pdfs(config: Dict[str, Any]) -> int: _UPDATE_CHECK_FILE = _runtime_dir() / "update_check.json" _INSTALLER_PATTERN = re.compile(r"^AnwesenheitPDF-Setup-(\d+\.\d+\.\d+)\.exe$", re.IGNORECASE) +_SOURCE_EXCEL_DIR = Path( + r"j:\Fachbereiche\BO\Produktionsschule\Maßnahmeunterlagen\30_Teilnehmende\01_Anwesenheit\Anwesenheitsliste" +) +_SOURCE_YEAR_PATTERN = re.compile(r"(20\d{2})") +_SOURCE_CHECK_INTERVAL_MS = 60_000 def _parse_semver(version_str: str) -> Tuple[int, int, int]: @@ -173,6 +178,90 @@ def _find_latest_installer(config: Dict[str, Any]) -> Optional[Tuple[str, Path]] return None +def _extract_year_from_excel_name(filename: str) -> Optional[int]: + """Extract a four-digit year from an Excel filename.""" + match = _SOURCE_YEAR_PATTERN.search(filename) + if not match: + return None + try: + return int(match.group(1)) + except ValueError: + return None + + +def _choose_source_excel(files: List[Path]) -> Optional[Path]: + """Pick the most relevant source Excel (current year preferred).""" + if not files: + return None + + current_year = date.today().year + with_year: List[Tuple[Path, int]] = [] + without_year: List[Path] = [] + + for file in files: + year = _extract_year_from_excel_name(file.name) + if year is None: + without_year.append(file) + else: + with_year.append((file, year)) + + current_year_files = [f for f, y in with_year if y == current_year] + if current_year_files: + return max(current_year_files, key=lambda p: p.stat().st_mtime) + + if with_year: + newest_year = max(y for _, y in with_year) + newest_year_files = [f for f, y in with_year if y == newest_year] + return max(newest_year_files, key=lambda p: p.stat().st_mtime) + + return max(without_year, key=lambda p: p.stat().st_mtime) + + +def _check_source_excel_update() -> Dict[str, Any]: + """Check source folder connectivity and whether a newer Excel is available.""" + result: Dict[str, Any] = { + "connected": False, + "update_available": False, + "source_file": None, + "target_file": None, + "status": "", + } + + try: + if not _SOURCE_EXCEL_DIR.exists(): + result["status"] = "Netzlaufwerk nicht erreichbar." + return result + + source_files = sorted(_SOURCE_EXCEL_DIR.glob("*.xlsx"), key=lambda p: p.name.lower()) + result["connected"] = True + + if not source_files: + result["status"] = "Keine Excel-Datei in der Quelle gefunden." + return result + + source_file = _choose_source_excel(source_files) + if source_file is None: + result["status"] = "Keine geeignete Excel-Datei in der Quelle gefunden." + return result + + target_file = _input_dir() / source_file.name + result["source_file"] = source_file + result["target_file"] = target_file + + if (not target_file.exists()) or (source_file.stat().st_mtime > target_file.stat().st_mtime): + result["update_available"] = True + result["status"] = f"Neue Excel verfügbar: {source_file.name}" + return result + + result["status"] = f"Excel aktuell: {source_file.name}" + return result + + except OSError: + result["connected"] = False + result["status"] = "Netzlaufwerk nicht erreichbar." + return result + + def _build_month_keys(config: Dict[str, Any]) -> List[str]: """Return deduplicated month keys from config (preserves insertion order).""" seen: set = set() @@ -248,6 +337,7 @@ class AnwesenheitApp(ctk.CTk): self._out_dir: Optional[Path] = None self._input_files: List[Path] = [] self._excel_name_to_path: Dict[str, Path] = {} + self._pending_source_excel: Optional[Path] = None self._tooltip_window: Optional[ctk.CTkToplevel] = None self._tooltip_label: Optional[tk.Label] = None @@ -258,8 +348,8 @@ class AnwesenheitApp(ctk.CTk): self._build_preview_section() self._build_log_section() - # Auto-sync after event loop starts - self.after(100, self._start_initial_sync) + # Initial scan + continuous source check + self.after(100, self._start_source_excel_watch) # Daily update check if _should_check_update(): self.after(200, self._start_update_check) @@ -348,6 +438,21 @@ class AnwesenheitApp(ctk.CTk): ) self._excel_menu.grid(row=0, column=2, padx=(0, 12), pady=6, sticky="w") + self._btn_excel_update = ctk.CTkButton( + frame, + text="Update", + width=90, + height=28, + command=self._on_excel_update_click, + ) + self._btn_excel_update.grid(row=0, column=3, padx=(0, 6), pady=6, sticky="w") + self._btn_excel_update.grid_remove() + + self._lbl_source_warning = ctk.CTkLabel( + frame, text="", width=24, anchor="w", text_color="orange" + ) + self._lbl_source_warning.grid(row=0, column=4, padx=(0, 12), pady=6, sticky="w") + # Row 1: Template ctk.CTkLabel(frame, text="Template:", width=110, anchor="w").grid( row=1, column=0, padx=(12, 4), pady=6, sticky="w" @@ -557,6 +662,32 @@ class AnwesenheitApp(ctk.CTk): self._excel_menu.configure(values=[text], state="disabled") self._excel_var.set(text) + def _set_source_state( + self, + connected: bool, + update_available: bool, + source_file: Optional[Path], + status: str, + ) -> None: + self._pending_source_excel = source_file if update_available else None + + if not connected: + self._lbl_source_warning.configure(text="⚠") + self._btn_excel_update.grid_remove() + self._set_download_status(status or "Netzlaufwerk nicht erreichbar.", "orange") + return + + self._lbl_source_warning.configure(text="") + if update_available and source_file is not None: + self._btn_excel_update.grid() + self._btn_excel_update.configure(state="normal", text="Update") + self._set_download_status(status, "#60A5FA") + return + + self._btn_excel_update.grid_remove() + if status: + self._set_download_status(status, "#22C55E") + def _set_download_status(self, text: str, color: str = "#60A5FA") -> None: self._lbl_download_status.configure(text=text, text_color=color) @@ -703,33 +834,78 @@ class AnwesenheitApp(ctk.CTk): self._excel_var.set(selected_name) self._excel_path = self._excel_name_to_path[selected_name] - def _start_initial_sync(self) -> None: - self._set_excel_loading_state("Lade Excel vom Netzlaufwerk ...") - self._set_download_status("Datei vom Netzlaufwerk wird geprueft ...") - threading.Thread(target=self._run_initial_sync, daemon=True).start() + def _start_source_excel_watch(self) -> None: + self._scan_and_validate() + self._start_source_excel_check() - def _run_initial_sync(self) -> None: + def _start_source_excel_check(self) -> None: + threading.Thread(target=self._run_source_excel_check, daemon=True).start() + + def _run_source_excel_check(self) -> None: + result = _check_source_excel_update() + self.after(0, self._on_source_excel_check_done, result) + + def _on_source_excel_check_done(self, result: Dict[str, Any]) -> None: + self._set_source_state( + connected=bool(result.get("connected")), + update_available=bool(result.get("update_available")), + source_file=result.get("source_file"), + status=str(result.get("status") or ""), + ) + self.after(_SOURCE_CHECK_INTERVAL_MS, self._start_source_excel_check) + + def _on_excel_update_click(self) -> None: + source_file = self._pending_source_excel + if source_file is None: + return + + target_file = _input_dir() / source_file.name + if target_file.exists(): + proceed = messagebox.askokcancel( + "Datei überschreiben", + f"Die vorhandene Datei\n{target_file.name}\n" + "wird durch die neuere Datei ersetzt.\n\nFortfahren?", + icon="warning", + parent=self, + ) + if not proceed: + return + + self._btn_excel_update.configure(state="disabled", text="Lade...") + self._set_download_status(f"Lade {source_file.name} ...", "#60A5FA") + threading.Thread( + target=self._run_source_excel_download, + args=(source_file,), + daemon=True, + ).start() + + def _run_source_excel_download(self, source_file: Path) -> None: try: - synced_path, status = _sync_network_excel(_CONFIG) - self.after(0, self._finish_initial_sync, synced_path, status, None) + target_file = _copy_excel_to_input(source_file) + self.after(0, self._on_source_excel_download_done, target_file, None) except Exception as exc: - self.after(0, self._finish_initial_sync, None, "", str(exc)) + self.after(0, self._on_source_excel_download_done, None, str(exc)) - def _finish_initial_sync( + def _on_source_excel_download_done( self, - synced_path: Optional[Path], - status: str, + target_file: Optional[Path], error: Optional[str], ) -> None: + self._btn_excel_update.configure(state="normal", text="Update") + if error: - self._set_download_status( - f"Netzlaufwerk-Pruefung fehlgeschlagen: {error}", - "orange", - ) - else: - color = "#22C55E" if synced_path else "orange" - self._set_download_status(status, color) - self._scan_and_validate(selected_excel=synced_path) + self._set_download_status(f"Update fehlgeschlagen: {error}", "red") + return + + if target_file is None: + self._set_download_status("Update fehlgeschlagen.", "red") + return + + self._set_download_status(f"Excel aktualisiert: {target_file.name}", "#22C55E") + self._refresh_excel_dropdown(selected=target_file) + self._on_excel_changed() + self._pending_source_excel = None + self._btn_excel_update.grid_remove() def _on_excel_changed(self) -> None: selected = self._excel_name_to_path.get(self._excel_var.get())