# -*- coding: utf-8 -*- """ ITD Transport Tracking - основен екран. Поле за четене на QR карта, регистрация по пост; генериране на QR карта; таблица с цикли. """ import os import sys import platform import subprocess import tkinter as tk from tkinter import ttk, filedialog # работима директория = тази на скрипта (при .exe = папката на exe) # Забележка: НЕ пипаме sys.path преди локалните импорти; PyInstaller има собствен importer. if getattr(sys, "frozen", False): SCRIPT_DIR = os.path.dirname(sys.executable) else: SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) from itd_db import ( get_connection, get_last_appsettings_path, get_last_connection_string_redacted, load_cycles, load_cards, close_cycle_manually, undo_manual_cycle_close, get_card_by_code, add_registration, create_card, update_card, delete_card, get_next_post_index, has_open_cycle, set_card_active, utc_to_local_for_display, ) os.chdir(SCRIPT_DIR) POST_NAMES = { 1: "1 – Портал (паркинг)", 2: "2 – Вход в завода", 3: "3 – Рампа начало товарене", 4: "4 – Рампа край товарене", 5: "5 – Напускане", } def _append_log(line: str): try: log_path = os.path.join(SCRIPT_DIR, "itd_transport.log") with open(log_path, "a", encoding="utf-8") as f: f.write(line.rstrip() + "\n") except Exception: pass def _center_on_parent(dialog, parent): """Същата логика като при „Генерирай QR карта“ – центриране в средата на parent.""" dialog.update_idletasks() parent.update_idletasks() w = dialog.winfo_width() h = dialog.winfo_height() if w < 50 or h < 50: w = dialog.winfo_reqwidth() h = dialog.winfo_reqheight() pw = parent.winfo_width() ph = parent.winfo_height() px = parent.winfo_rootx() py = parent.winfo_rooty() if pw < 50 or ph < 50: sw = dialog.winfo_screenwidth() sh = dialog.winfo_screenheight() x = max(0, (sw - w) // 2) y = max(0, (sh - h) // 2) else: x = px + (pw - w) // 2 y = py + (ph - h) // 2 dialog.geometry(f"+{x}+{y}") def _msg_centered(parent, title, message, icon_type="info"): """Показва модален диалог с една бутон OK – центриране като при „Генерирай QR карта“.""" win = tk.Toplevel(parent) win.withdraw() win.transient(parent) win.title(title) win.resizable(False, False) f = ttk.Frame(win, padding=20) f.pack(fill=tk.BOTH, expand=True) ttk.Label(f, text=message, wraplength=420).pack(pady=(0, 16)) ttk.Button(f, text="OK", command=win.destroy).pack() _center_on_parent(win, parent) win.deiconify() win.grab_set() win.focus_set() win.wait_window() def _ask_yesno_centered(parent, title, message): """Показва модален Да/Не диалог – центриране като при „Генерирай QR карта“. Връща True за Да, False за Не.""" result = [False] def on_yes(): result[0] = True win.destroy() def on_no(): result[0] = False win.destroy() win = tk.Toplevel(parent) win.withdraw() win.transient(parent) win.title(title) win.resizable(False, False) f = ttk.Frame(win, padding=20) f.pack(fill=tk.BOTH, expand=True) ttk.Label(f, text=message, wraplength=420).pack(pady=(0, 16)) bf = ttk.Frame(f) bf.pack() ttk.Button(bf, text="Да", command=on_yes).pack(side=tk.LEFT, padx=6) ttk.Button(bf, text="Не", command=on_no).pack(side=tk.LEFT, padx=6) _center_on_parent(win, parent) win.deiconify() win.grab_set() win.focus_set() win.wait_window() return result[0] def main(): root = tk.Tk() root.withdraw() try: conn = get_connection() except Exception as e: cfg_path = get_last_appsettings_path() cs = get_last_connection_string_redacted() _append_log(f"[STARTUP] DB connect failed. appsettings={cfg_path or 'N/A'}; conn={cs or 'N/A'}; error={e!s}") root.deiconify() root.update_idletasks() extra = f"\n\nИзползван appsettings.json:\n{cfg_path}" if cfg_path else "" _msg_centered(root, "Грешка", f"Не може да се свърже с базата:\n{e}{extra}") try: root.destroy() except Exception: pass return 1 cfg_path = get_last_appsettings_path() cs = get_last_connection_string_redacted() _append_log(f"[STARTUP] OK. appsettings={cfg_path or 'N/A'}; conn={cs or 'N/A'}") # Автоматично създаване на таблици при липса (трансфер на нов сървер) try: from db_check import run_schema, check_tables _base = getattr(sys, "_MEIPASS", SCRIPT_DIR) schema_path = os.path.join(_base, "schema.sql") if os.path.isfile(schema_path): run_schema(conn.cursor(), schema_path) has_cards, has_cycles = check_tables(conn.cursor()) if not (has_cards and has_cycles): root.deiconify() root.update_idletasks() _msg_centered(root, "Грешка", "След изпълнение на схемата липсват таблици ITD_Cards или ITD_Cycles.") return 1 except Exception as e: root.deiconify() root.update_idletasks() _msg_centered(root, "Грешка", f"Грешка при проверка/създаване на таблици:\n{e}") return 1 root.deiconify() root.title("ITD Transport Tracking") root.minsize(800, 400) root.geometry("1000x560") root.configure(bg="white") # Икона на приложението – използваме само ITD.ico в папката на скрипта icon_path = os.path.join(SCRIPT_DIR, "ITD.ico") if os.path.isfile(icon_path): try: root.iconbitmap(icon_path) except Exception: pass # PNG икона (за платформи, където .ico не се поддържа) for icon_name in ("icon.png", "app.png"): icon_path = os.path.join(SCRIPT_DIR, icon_name) if os.path.isfile(icon_path): try: from PIL import Image img = Image.open(icon_path) from PIL import ImageTk root.iconphoto(True, ImageTk.PhotoImage(img)) except Exception: pass break # --- Заглавна лента: по-едър шрифт, светлосин фон, текст центриран --- title_frame = tk.Frame(root, bg="#93C5FD", height=52) title_frame.grid(row=0, column=0, columnspan=2, sticky="ew") title_frame.grid_propagate(False) root.grid_columnconfigure(0, weight=1) title_label = tk.Label( title_frame, text="ITD Transport Tracking", font=("Segoe UI", 16, "bold"), fg="#1E3A8A", bg="#93C5FD", ) title_label.place(relx=0.5, rely=0.5, anchor="center") # --- Горна лента: четене на QR карта и пост --- scan_frame = ttk.LabelFrame(root, text="", padding=6) scan_frame.grid(row=1, column=0, columnspan=2, sticky="ew", padx=4, pady=4) root.grid_columnconfigure(0, weight=1) ttk.Label(scan_frame, text="Постове / Терминали:").grid(row=0, column=0, padx=(0, 6), pady=2, sticky="w") posts_frame = ttk.Frame(scan_frame) posts_frame.grid(row=0, column=1, padx=0, pady=2, sticky="w") post_colors = { 1: "#BFDBFE", # по-наситено синьо 2: "#BBF7D0", # по-наситено зелено 3: "#FDE68A", # по-наситено златисто 4: "#DDD6FE", # по-наситено лилаво 5: "#FCA5A5", # по-наситено червено } for i in range(1, 6): lbl = tk.Label( posts_frame, text=POST_NAMES[i], padx=10, pady=6, font=("Segoe UI", 9), fg="#111827", bg=post_colors.get(i, "#4F4F4F"), bd=0, relief="flat", highlightthickness=1, highlightbackground="#D1D5DB", ) lbl.pack(side=tk.LEFT, padx=(0, 6)) ttk.Label(scan_frame, text="QR карта (код):").grid(row=1, column=0, padx=(0, 4), pady=(6, 4), sticky="w") qr_row_frame = ttk.Frame(scan_frame) qr_row_frame.grid(row=1, column=1, padx=0, pady=(6, 4), sticky="ew") scan_frame.columnconfigure(1, weight=1) qr_entry = ttk.Entry(qr_row_frame, width=40, font=("Segoe UI", 10)) qr_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 8), ipady=2) def on_register(): code = qr_entry.get().strip() if not code: root.update_idletasks() _msg_centered(root, "Информация", "Въведете или сканирайте кода на картата.") qr_entry.focus_set() return card = get_card_by_code(conn, code) if not card: root.update_idletasks() _msg_centered(root, "Внимание", f"Карта с код „{code}” не е намерена или е неактивна.") qr_entry.select_range(0, tk.END) qr_entry.focus_set() return try: post_idx = get_next_post_index(conn, card["id"]) if post_idx == 1 and has_open_cycle(conn, card["id"]): root.update_idletasks() _msg_centered( root, "Внимание", "Вече сте влезли. Моля, излезте преди нова регистрация.", ) qr_entry.select_range(0, tk.END) qr_entry.focus_set() return add_registration(conn, card["id"], post_idx) qr_entry.delete(0, tk.END) qr_entry.focus_set() refresh() root.update_idletasks() _msg_centered(root, "Готово", f"Регистрирано: {card['display_text']} на пост {post_idx}.") except Exception as e: root.update_idletasks() _msg_centered(root, "Грешка", f"Неуспешна регистрация:\n{e}") ttk.Button(qr_row_frame, text="Регистрирай", command=on_register).pack(side=tk.LEFT) qr_entry.bind("", lambda e: on_register()) def open_qr_dialog(): win = tk.Toplevel(root) win.title("Генериране на QR карта") win.transient(root) win.grab_set() f = ttk.Frame(win, padding=12) f.pack(fill=tk.BOTH, expand=True) ttk.Label(f, text="Код (ID):").grid(row=0, column=0, sticky="w", pady=2) code_label = ttk.Label(f, text="—", foreground="gray") code_label.grid(row=0, column=1, padx=8, pady=2, sticky="w") ttk.Label(f, text="(задава се автоматично след запис в базата)").grid( row=0, column=2, sticky="w", pady=2 ) ttk.Label(f, text="Текст на картата (име + № кола):").grid(row=1, column=0, sticky="w", pady=2) text_ent = ttk.Entry(f, width=35) text_ent.grid(row=1, column=1, padx=8, pady=2, columnspan=2, sticky="ew") f.columnconfigure(1, weight=1) save_to_db_var = tk.BooleanVar(value=True) ttk.Checkbutton(f, text="Запиши карта в базата", variable=save_to_db_var).grid( row=2, column=0, columnspan=2, sticky="w", pady=4 ) qr_label = ttk.Label(f, text="") qr_label.grid(row=3, column=0, columnspan=2, pady=8) def do_generate(): display_text = text_ent.get().strip() if not display_text: win.update_idletasks() _msg_centered(win, "Внимание", "Въведете текст на картата.") text_ent.focus_set() return if not save_to_db_var.get(): win.update_idletasks() _msg_centered( win, "Внимание", "За да генерирате QR карта с код, отметнете „Запиши карта в базата“.", ) return try: new_id = create_card(conn, display_text) code = str(new_id) code_label.config(text=code, foreground="#111827") import qrcode from PIL import ImageTk qr = qrcode.QRCode(version=1, box_size=6, border=2) qr.add_data(code) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") img = img.convert("RGB").resize((220, 220)) ph = ImageTk.PhotoImage(img) qr_label.configure(image=ph, text="") qr_label.image = ph win.update_idletasks() _msg_centered(win, "Готово", f"Картата е записана с код {code}. Отпечатайте QR кода по-долу.") except Exception as ex: win.update_idletasks() _msg_centered(win, "Грешка", str(ex)) ttk.Button(f, text="Генерирай QR", command=do_generate).grid(row=4, column=0, columnspan=2, pady=6) # центриране на прозореца в средата на главния прозорец win.update_idletasks() root.update_idletasks() w = win.winfo_width() h = win.winfo_height() rw = root.winfo_width() rh = root.winfo_height() rx = root.winfo_rootx() ry = root.winfo_rooty() x = rx + (rw - w) // 2 y = ry + (rh - h) // 2 win.geometry(f"+{x}+{y}") text_ent.focus_set() # Филтър по номер на карта filter_frame = ttk.Frame(root) filter_frame.grid(row=2, column=0, columnspan=2, sticky="ew", padx=4, pady=(0, 4)) root.grid_columnconfigure(0, weight=1) ttk.Label(filter_frame, text="Филтър по карта (номер):").pack(side=tk.LEFT, padx=(0, 6)) filter_entry = ttk.Entry(filter_frame, width=14, font=("Segoe UI", 10)) filter_entry.pack(side=tk.LEFT, padx=(0, 6)) card_filter = [None] # mutable, за достъп от вложени функции def apply_filter(): card_filter[0] = (filter_entry.get() or "").strip() or None refresh() def clear_filter(): filter_entry.delete(0, tk.END) card_filter[0] = None refresh() ttk.Button(filter_frame, text="Филтрирай", command=apply_filter).pack(side=tk.LEFT, padx=2) ttk.Button(filter_frame, text="Всички", command=clear_filter).pack(side=tk.LEFT, padx=2) # платно за визуализация на цикли с цветни постове row_height = 40 canvas = tk.Canvas(root, background="white") scroll_y = ttk.Scrollbar(root, orient=tk.VERTICAL, command=canvas.yview) canvas.configure(yscrollcommand=scroll_y.set) # данни за редовете (card_id, cycle_started_at и др.) cycles_data = [] selected_row_index = None post_colors = { 1: "#BFDBFE", # по-наситено синьо 2: "#BBF7D0", # по-наситено зелено 3: "#FDE68A", # по-наситено златисто 4: "#DDD6FE", # по-наситено лилаво 5: "#FCA5A5", # по-наситено червено } def draw_rows(): canvas.delete("all") width = canvas.winfo_width() or 1000 card_width = 200 post_width = 108 gap = 4 # Служебен изход / изход веднага след пост 5 after_posts_x = card_width + 5 * (post_width + gap) exit_x = after_posts_x + 8 for idx, r in enumerate(cycles_data): y0 = idx * row_height y1 = y0 + row_height # фон на реда (за селекция) bg_color = "#E5E7EB" if idx == selected_row_index else "#FFFFFF" canvas.create_rectangle(0, y0, width, y1, fill=bg_color, outline="#E5E7EB") # текст на картата: "код" - "име" card_label = f'{r.get("code", "")} - {r["display_text"]}' if r.get("code") else r["display_text"] canvas.create_text( 8, (y0 + y1) / 2, text=card_label, anchor="w", font=("Segoe UI", 9), fill="#111827", ) # 5 цветни правоъгълника за постовете for p in range(1, 6): x0 = card_width + (p - 1) * (post_width + gap) x1 = x0 + post_width txt = r.get(f"post{p}", "") canvas.create_rectangle( x0, y0 + 4, x1, y1 - 4, fill=post_colors.get(p, "#9CA3AF"), outline="#E5E7EB", ) canvas.create_text( (x0 + x1) / 2, (y0 + y1) / 2, text=txt, anchor="center", font=("Segoe UI", 8), fill="#111827", ) # изход / служебен изход (веднага след пост 5) exit_text = r.get("exit_datetime", "") exit_fill = "#B91C1C" if r.get("service_exit") else "#111827" canvas.create_text( exit_x, (y0 + y1) / 2, text=exit_text, anchor="w", font=("Segoe UI", 9), fill=exit_fill, ) canvas.configure(scrollregion=canvas.bbox("all")) def refresh(event=None): nonlocal cycles_data, selected_row_index cycles_data.clear() selected_row_index = None try: rows = load_cycles(conn) except Exception as e: root.update_idletasks() _msg_centered(root, "Грешка", f"Грешка при зареждане:\n{e}") return cf = card_filter[0] if cf: cf_str = str(cf).strip() rows = [r for r in rows if str(r.get("code", "")).strip() == cf_str] for r in rows: cycles_data.append(r) status_label.config(text=f"Заредени {len(cycles_data)} цикъла" + (f" (филтър: {cf})" if cf else "")) draw_rows() def on_canvas_click(event): nonlocal selected_row_index y = canvas.canvasy(event.y) idx = int(y // row_height) if 0 <= idx < len(cycles_data): selected_row_index = idx draw_rows() canvas.bind("", on_canvas_click) canvas.bind("", refresh) def open_cards_dialog(): """Модул 'Карти' – списък на всички карти с (де)активиране.""" win = tk.Toplevel(root) win.title("Карти") win.transient(root) win.grab_set() win.configure(bg="#FFFDE7") root.update_idletasks() rw = root.winfo_width() rh = root.winfo_height() w_win = max(520, min(rw - 80, 920)) h_win = max(400, min(rh - 80, 620)) win.geometry(f"{w_win}x{h_win}") frame = tk.Frame(win, bg="#FFFDE7", padx=12, pady=12) frame.pack(fill=tk.BOTH, expand=True) # Таблица като решетка: сиво очертаване (не черно) cell_bg = "#FFFDE7" header_bg = "#F5F0D7" selected_bg = "#E8E0C8" table_border_gray = "#9CA3AF" col_widths = (18, 32, 10, 18, 18) # ширина в знаци canvas_wrap = tk.Canvas(frame, bg=cell_bg, highlightthickness=0) scroll_cards = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=canvas_wrap.yview) border_frame = tk.Frame(canvas_wrap, bg=table_border_gray, padx=1, pady=1) table_frame = tk.Frame(border_frame, bg=cell_bg) table_frame.pack(fill=tk.BOTH, expand=True) table_frame.bind( "", lambda e: canvas_wrap.configure(scrollregion=canvas_wrap.bbox("all")), ) canvas_wrap.create_window((0, 0), window=border_frame, anchor="nw") canvas_wrap.configure(yscrollcommand=scroll_cards.set) def _on_canvas_configure(event): for item in canvas_wrap.find_all(): canvas_wrap.itemconfig(item, width=event.width) canvas_wrap.bind("", _on_canvas_configure) cards_data = [] selected_row_index = [None] # list to allow assign in nested func def fmt_dt_short(dt): if not dt: return "" try: if hasattr(dt, "strftime"): return dt.strftime("%d.%m.%Y %H:%M") s = str(dt) return s.split(".")[0].replace("T", " ") except Exception: return str(dt) def clear_table(): for w in table_frame.grid_slaves(): w.destroy() def on_select_row(idx): selected_row_index[0] = idx rebuild_table() def rebuild_table(): clear_table() # Ред 0: заглавки – всяка клетка в сива рамка (не черна) headers = ("Код", "Текст (име + № кола)", "Активна", "Създадена на", "Деактивирана на") for c, (text, wch) in enumerate(zip(headers, col_widths)): cell_f = tk.Frame(table_frame, bg=table_border_gray, padx=1, pady=1) cell_f.grid(row=0, column=c, sticky="nsew", padx=0, pady=0) lbl = tk.Label( cell_f, text=text, width=wch, font=("Segoe UI", 9, "bold"), bg=header_bg, anchor="center", ) lbl.pack(fill=tk.BOTH, expand=True) table_frame.columnconfigure(1, weight=1) for r_idx, r in enumerate(cards_data): deact_date = "" if not r["is_active"] and r.get("updated_at"): deact_date = fmt_dt_short(utc_to_local_for_display(r["updated_at"])) row_bg = selected_bg if selected_row_index[0] == r_idx else cell_bg vals = ( r["code"], r["display_text"], "Да" if r["is_active"] else "Не", fmt_dt_short(utc_to_local_for_display(r["created_at"])), deact_date, ) for c, (val, wch) in enumerate(zip(vals, col_widths)): cell_f = tk.Frame(table_frame, bg=table_border_gray, padx=1, pady=1) cell_f.grid(row=r_idx + 1, column=c, sticky="nsew", padx=0, pady=0) lbl = tk.Label( cell_f, text=val or "", width=wch, font=("Segoe UI", 9), bg=row_bg, anchor="w" if c != 2 else "center", ) lbl.pack(fill=tk.BOTH, expand=True) cell_f.bind("", lambda e, i=r_idx: on_select_row(i)) lbl.bind("", lambda e, i=r_idx: on_select_row(i)) def reload_cards(): cards_data.clear() selected_row_index[0] = None try: rows = load_cards(conn) except Exception as ex: _msg_centered(win, "Грешка", f"Грешка при зареждане на карти:\n{ex}") return for r in rows: cards_data.append(r) rebuild_table() def get_selected_card(): idx = selected_row_index[0] if idx is None or idx < 0 or idx >= len(cards_data): _msg_centered(win, "Информация", "Изберете карта от списъка (клик върху ред).") return None return cards_data[idx] def on_deactivate_card(): card = get_selected_card() if not card: return if not card["is_active"]: _msg_centered(win, "Информация", "Картата вече е деактивирана.") return if not _ask_yesno_centered( win, "Потвърждение", "Деактивирате ли тази карта?\n\nСлед деактивация тя няма да може да се използва за регистрация.", ): return try: set_card_active(conn, card["id"], False) _msg_centered(win, "Готово", "Картата е деактивирана.") reload_cards() refresh() except Exception as ex: _msg_centered(win, "Грешка", f"Грешка при деактивиране:\n{ex}") def on_activate_card(): card = get_selected_card() if not card: return if card["is_active"]: _msg_centered(win, "Информация", "Картата вече е активна.") return if not _ask_yesno_centered( win, "Потвърждение", "Активирате ли тази карта отново?", ): return try: set_card_active(conn, card["id"], True) _msg_centered(win, "Готово", "Картата е активирана отново.") reload_cards() refresh() except Exception as ex: _msg_centered(win, "Грешка", f"Грешка при активиране:\n{ex}") def create_card_png(): card = get_selected_card() if not card: return path = filedialog.asksaveasfilename( title="Запази карта като PNG", defaultextension=".png", filetypes=[("PNG изображение", "*.png"), ("Всички файлове", "*.*")], initialfile=f"karta_{card['code']}.png", ) if not path: return try: import qrcode from PIL import Image, ImageDraw, ImageFont code = str(card["code"]) text = str(card["display_text"] or "") # Размер като визитка (прибл. 90×54 mm при 300 dpi), QR колкото може по-голям w, h = 1063, 638 # ~90×54 mm @ 300 dpi img = Image.new("RGB", (w, h), "white") draw = ImageDraw.Draw(img) draw.rectangle([(0, 0), (w - 1, h - 1)], outline="black", width=2) margin = 16 # QR заема максимално височината на картата qr_size = h - 2 * margin qr = qrcode.QRCode(version=1, box_size=10, border=2) qr.add_data(code) qr.make(fit=True) qr_img = qr.make_image(fill_color="black", back_color="white").convert("RGB") qr_img = qr_img.resize((qr_size, qr_size)) qr_x = w - margin - qr_size qr_y = margin img.paste(qr_img, (qr_x, qr_y)) # Текст вляво – компактен try: font_code = ImageFont.truetype("arial.ttf", 26) font_label = ImageFont.truetype("arial.ttf", 14) font_text = ImageFont.truetype("arial.ttf", 20) except Exception: font_code = font_label = font_text = ImageFont.load_default() tx, ty = margin, margin draw.text((tx, ty), f"Код: {code}", fill="black", font=font_code) ty += 38 draw.text((tx, ty), "Име / № кола:", fill="gray", font=font_label) ty += 22 max_chars = max(12, (qr_x - tx - 12) // 12) for line in (text[i : i + max_chars] for i in range(0, len(text), max_chars)): draw.text((tx, ty), line, fill="black", font=font_text) ty += 26 img.save(path) win.update_idletasks() _msg_centered(win, "Готово", f"Файлът е запазен:\n{path}") # Отваряне на файла с програмата по подразбиране за преглед try: path_abs = os.path.abspath(path) if platform.system() == "Windows": os.startfile(path_abs) elif platform.system() == "Darwin": subprocess.run(["open", path_abs], check=False) else: subprocess.run(["xdg-open", path_abs], check=False) except Exception: pass except Exception as ex: win.update_idletasks() _msg_centered(win, "Грешка", f"Неуспешно създаване на PNG:\n{ex}") def on_edit_card(): card = get_selected_card() if not card: return edit_win = tk.Toplevel(win) edit_win.title("Коригиране на карта") edit_win.transient(win) edit_win.grab_set() f_edit = ttk.Frame(edit_win, padding=12) f_edit.pack(fill=tk.BOTH, expand=True) ttk.Label(f_edit, text="Код (ID):").grid(row=0, column=0, sticky="w", pady=2) ttk.Label(f_edit, text=card["code"]).grid(row=0, column=1, padx=8, pady=2, sticky="w") ttk.Label(f_edit, text="Текст (име + № кола):").grid(row=1, column=0, sticky="w", pady=2) text_ent = ttk.Entry(f_edit, width=40) text_ent.insert(0, card["display_text"] or "") text_ent.grid(row=1, column=1, padx=8, pady=2, sticky="ew") f_edit.columnconfigure(1, weight=1) def do_save(): new_text = text_ent.get().strip() if not new_text: _msg_centered(edit_win, "Внимание", "Въведете текст на картата.") return try: update_card(conn, card["id"], new_text) _msg_centered(edit_win, "Готово", "Картата е коригирана.") edit_win.destroy() reload_cards() refresh() except Exception as ex: _msg_centered(edit_win, "Грешка", str(ex)) btn_row = ttk.Frame(f_edit) btn_row.grid(row=2, column=0, columnspan=2, pady=(12, 0)) ttk.Button(btn_row, text="Запази", command=do_save).pack(side=tk.LEFT, padx=4) ttk.Button(btn_row, text="Отказ", command=edit_win.destroy).pack(side=tk.LEFT, padx=4) _center_on_parent(edit_win, win) text_ent.focus_set() def on_delete_card(): card = get_selected_card() if not card: return if not _ask_yesno_centered( win, "Потвърждение", f"Изтриване на карта?\n\nКод: {card['code']}\nТекст: {card['display_text']}\n\nЩе бъдат изтрити и всички регистрации по тази карта.", ): return try: delete_card(conn, card["id"]) _msg_centered(win, "Готово", "Картата е изтрита.") reload_cards() refresh() except Exception as ex: _msg_centered(win, "Грешка", f"Грешка при изтриване:\n{ex}") canvas_wrap.grid(row=0, column=0, sticky="nsew") scroll_cards.grid(row=0, column=1, sticky="ns") frame.rowconfigure(0, weight=1) frame.columnconfigure(0, weight=1) btn_bar = ttk.Frame(frame) btn_bar.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(8, 0)) ttk.Button(btn_bar, text="Обнови", command=reload_cards).pack(side=tk.LEFT, padx=4) ttk.Button(btn_bar, text="Коригирай", command=on_edit_card).pack(side=tk.LEFT, padx=4) ttk.Button(btn_bar, text="Изтрий", command=on_delete_card).pack(side=tk.LEFT, padx=4) ttk.Button(btn_bar, text="Деактивирай", command=on_deactivate_card).pack(side=tk.LEFT, padx=4) ttk.Button(btn_bar, text="Активирай", command=on_activate_card).pack(side=tk.LEFT, padx=4) ttk.Button(btn_bar, text="Създай PNG", command=create_card_png).pack(side=tk.LEFT, padx=4) ttk.Button(btn_bar, text="Генерирай QR карта", command=open_qr_dialog).pack(side=tk.LEFT, padx=4) ttk.Button(btn_bar, text="Затвори", command=win.destroy).pack(side=tk.RIGHT, padx=4) reload_cards() _center_on_parent(win, root) def on_close_cycle(): nonlocal selected_row_index if selected_row_index is None or selected_row_index >= len(cycles_data): root.update_idletasks() _msg_centered(root, "Информация", "Изберете ред от списъка (клик с мишката).") return row = cycles_data[selected_row_index] root.update_idletasks() if not _ask_yesno_centered(root, "Потвърждение", "Служебно затваряне на цикъла за тази карта?"): return try: updated = close_cycle_manually(conn, row["cycle_id"]) root.update_idletasks() if updated: _msg_centered(root, "Готово", "Цикълът е затворен. Записан е Служебен изход.") else: _msg_centered(root, "Информация", "Цикълът вече е затворен служебно.") refresh() except Exception as e: root.update_idletasks() _msg_centered(root, "Грешка", f"Неуспешно затваряне:\n{e}") def on_undo_service_exit(): nonlocal selected_row_index if selected_row_index is None or selected_row_index >= len(cycles_data): root.update_idletasks() _msg_centered(root, "Информация", "Изберете ред от списъка (клик с мишката).") return row = cycles_data[selected_row_index] if not row.get("service_exit"): root.update_idletasks() _msg_centered(root, "Информация", "Изберете цикъл със служебен изход за отмяна.") return root.update_idletasks() if not _ask_yesno_centered(root, "Потвърждение", "Да се премахне служебният изход за този цикъл?"): return try: removed = undo_manual_cycle_close(conn, row["cycle_id"]) root.update_idletasks() if removed: _msg_centered(root, "Готово", "Служебният изход е отменен.") else: _msg_centered(root, "Информация", "Записът за служебен изход вече липсва.") refresh() except Exception as e: root.update_idletasks() _msg_centered(root, "Грешка", f"Неуспешна отмяна:\n{e}") # бутони и статус btn_frame = ttk.Frame(root) ttk.Button(btn_frame, text="Обнови", command=refresh).pack(side=tk.LEFT, padx=4) ttk.Button(btn_frame, text="Затвори цикъла", command=on_close_cycle).pack(side=tk.LEFT, padx=4) ttk.Button(btn_frame, text="Отмени служебен изход", command=on_undo_service_exit).pack(side=tk.LEFT, padx=4) ttk.Button(btn_frame, text="Карти", command=open_cards_dialog).pack(side=tk.LEFT, padx=12) status_label = ttk.Label(btn_frame, text="") status_label.pack(side=tk.LEFT, padx=12) # подредба canvas.grid(row=3, column=0, sticky="nsew") scroll_y.grid(row=3, column=1, sticky="ns") btn_frame.grid(row=4, column=0, columnspan=2, sticky="ew", pady=8, padx=4) root.grid_rowconfigure(3, weight=1) root.grid_columnconfigure(0, weight=1) refresh() root.mainloop() try: conn.close() except Exception: pass return 0 if __name__ == "__main__": sys.exit(main())