|
|
|
|
@ -0,0 +1,893 @@
|
|
|
|
|
# -*- 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("<Return>", 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("<Button-1>", on_canvas_click)
|
|
|
|
|
canvas.bind("<Configure>", 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(
|
|
|
|
|
"<Configure>",
|
|
|
|
|
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("<Configure>", _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("<Button-1>", lambda e, i=r_idx: on_select_row(i))
|
|
|
|
|
lbl.bind("<Button-1>", 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())
|