/
AxeOn
/
ProjectAi
Обзор
Документация
Войти
/
AxeOn
/
ProjectAi
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
ui.py
329 строк
18 KB
Дмитрий
refactor: configure log rotation and remove dead code
25 апр 2026, 15:41
25 апр 2026, 15:41
adcf4de
Код
Авторство
О чём код?
import tkinter as tk import ttkbootstrap as ttk from ttkbootstrap.constants import * from ttkbootstrap.tooltip import ToolTip from tkinter import messagebox from constants import TaskStatus, Priority, DateFilter class CommentDialog(ttk.Toplevel): def __init__(self, parent, title, initial_text, on_save_callback): super().__init__(parent) self.title(title) self.geometry("500x350") self.on_save = on_save_callback try: x = parent.winfo_x() + (parent.winfo_width() // 2) - 250 y = parent.winfo_y() + (parent.winfo_height() // 2) - 175 self.geometry(f"+{x}+{y}") except Exception: pass btn_frame = ttk.Frame(self, padding=10) btn_frame.pack(side="bottom", fill="x") # Кнопки диалога ttk.Button(btn_frame, text="💾 Сохранить", command=self.save, bootstyle="success").pack(side="right", padx=5) ttk.Button(btn_frame, text="Отмена", command=self.destroy, bootstyle="secondary").pack(side="right", padx=5) self.text_area = tk.Text(self, wrap="word", font=("Segoe UI", 11), bg="#4E5D6C", fg="white", insertbackground="white", padx=10, pady=10) self.text_area.pack(side="top", fill="both", expand=True, padx=10, pady=(10, 0)) self.text_area.insert("1.0", initial_text if initial_text else "") self.text_area.focus_set() def save(self): new_comment = self.text_area.get("1.0", "end-1c").strip() self.on_save(new_comment) self.destroy() class TaskView(ttk.Frame): def __init__(self, master, callbacks): super().__init__(master) self.callbacks = callbacks self.pack(fill="both", expand=True) self.dragged_item = None self.setup_ui() self.setup_context_menu() self.apply_shortcuts(self) self.status_bar = ttk.Label(self, text="Готов к работе", bootstyle="inverse-secondary", anchor="w", padding=(10, 2)) self.status_bar.pack(side="bottom", fill="x") master.bind("<Control-z>", lambda e: self.callbacks['undo']()) master.bind("<Control-Cyrillic_ya>", lambda e: self.callbacks['undo']()) def set_status(self, message, is_error=False): style = "inverse-danger" if is_error else "inverse-secondary" self.status_bar.config(text=message, bootstyle=style) if is_error: self.after(5000, lambda: self.status_bar.config(text="Готов к работе", bootstyle="inverse-secondary")) def setup_ui(self): control_frame = ttk.Frame(self, padding=10) control_frame.pack(fill="x") # Блок описания задачи desc_frame = ttk.Labelframe(control_frame, text="Задача", padding=10) desc_frame.pack(side="left", fill="both", expand=True, padx=5) self.entry_task = tk.Text(desc_frame, width=40, height=4, wrap="word", font=("Segoe UI", 10), bg="#4E5D6C", fg="white", insertbackground="white") self.entry_task.pack(side="left", fill="both", expand=True) # Кнопка микрофона voice_btn = ttk.Button(desc_frame, text="🎤", bootstyle="secondary", command=self.callbacks['voice'], width=3) voice_btn.pack(side="right", fill="y", padx=2) ToolTip(voice_btn, text="Голосовой ввод") # Блок параметров param_frame = ttk.Labelframe(control_frame, text="Параметры", padding=10) param_frame.pack(side="left", fill="both", padx=5) ttk.Label(param_frame, text="Приоритет:").grid(row=0, column=0, sticky="w") self.combo_priority = ttk.Combobox(param_frame, values=Priority.list_all(), state="readonly", width=14) self.combo_priority.current(1) self.combo_priority.grid(row=0, column=1, padx=5, pady=2) ttk.Label(param_frame, text="Исполнитель:").grid(row=1, column=0, sticky="w") self.combo_assignee = ttk.Combobox(param_frame, width=14) self.combo_assignee.grid(row=1, column=1, padx=5, pady=2) # --- [ИЗМЕНЕНИЕ] Кнопка настроек сотрудников УДАЛЕНА отсюда --- ttk.Label(param_frame, text="Срок:").grid(row=2, column=0, sticky="w") self.entry_date = ttk.DateEntry(param_frame, width=14, bootstyle="primary", firstweekday=0, dateformat="%d.%m.%Y") self.entry_date.grid(row=2, column=1, padx=5, pady=2) # --- ПАНЕЛЬ ДЕЙСТВИЙ --- action_frame = ttk.Frame(self, padding=10) action_frame.pack(fill="x") # === БЛОК СТАТИСТИКИ (Справа) === stats_frame = ttk.Frame(action_frame) stats_frame.pack(side="right", padx=(10, 0)) ttk.Separator(action_frame, orient="vertical").pack(side="right", fill="y", padx=10) # [ИЗМЕНЕНИЕ] Уменьшен шрифт до 10, добавлены отступы (padding) для сохранения размера плашки stat_font = ("Segoe UI", 10, "bold") stat_pad = (5, 5) self.lbl_stat_total = ttk.Label(stats_frame, text="📥 0", bootstyle="inverse-secondary", font=stat_font, padding=stat_pad) self.lbl_stat_total.pack(side="right", padx=8) self.lbl_stat_high = ttk.Label(stats_frame, text="🔥 0", bootstyle="inverse-danger", font=stat_font, padding=stat_pad) self.lbl_stat_high.pack(side="right", padx=8) self.lbl_stat_today = ttk.Label(stats_frame, text="⚠ 0", bootstyle="inverse-warning", font=stat_font, padding=stat_pad) self.lbl_stat_today.pack(side="right", padx=8) self.lbl_stat_overdue = ttk.Label(stats_frame, text="⏳ 0", bootstyle="inverse-danger", font=stat_font, padding=stat_pad) self.lbl_stat_overdue.pack(side="right", padx=8) # === КНОПКИ (Слева) === btn_frame = ttk.Frame(action_frame) btn_frame.pack(side="left", fill="x") # --- БЛОК 1: CRUD --- block1 = ttk.Frame(btn_frame) block1.pack(side="left") # Добавить задачу btn_add = ttk.Button(block1, text="✚", command=self.callbacks['add_root'], bootstyle="success", width=5) btn_add.pack(side="left", padx=3) ToolTip(btn_add, text="Добавить задачу", bootstyle="inverse-success") # Добавить подзадачу btn_sub = ttk.Button(block1, text="↳", command=self.callbacks['add_sub'], bootstyle="info", width=5) btn_sub.pack(side="left", padx=3) ToolTip(btn_sub, text="Добавить подзадачу", bootstyle="inverse-info") # Сохранить (активна при редактировании) self.btn_update = ttk.Button(block1, text="💾", command=self.callbacks['save_edit'], bootstyle="warning", state="disabled", width=5) self.btn_update.pack(side="left", padx=3) ToolTip(self.btn_update, text="Сохранить", bootstyle="inverse-warning") # Отмена btn_undo = ttk.Button(block1, text="↶", command=self.callbacks['undo'], bootstyle="secondary", width=5) btn_undo.pack(side="left", padx=3) ToolTip(btn_undo, text="Отмена", bootstyle="inverse-secondary") # Удалить btn_del = ttk.Button(block1, text="🗑", command=self.callbacks['delete'], bootstyle="danger", width=5) btn_del.pack(side="left", padx=3) ToolTip(btn_del, text="Удалить", bootstyle="inverse-danger") ttk.Separator(btn_frame, orient="vertical").pack(side="left", fill="y", padx=10) # --- БЛОК 2: TELEGRAM --- block2 = ttk.Frame(btn_frame) block2.pack(side="left") # Отправить в TG btn_tg = ttk.Button(block2, text="✈", command=self.callbacks['tg_send'], bootstyle="primary", width=5) btn_tg.pack(side="left", padx=3) ToolTip(btn_tg, text="Отправить задачу в TG", bootstyle="inverse-primary") # Отправить ВСЕ в TG btn_tg_all = ttk.Button(block2, text="✈✈", command=self.callbacks['tg_send_all'], bootstyle="primary", width=5) btn_tg_all.pack(side="left", padx=3) ToolTip(btn_tg_all, text="Отправить выбранные задачи в TG", bootstyle="inverse-primary") # Настройки рассылок TG btn_sync = ttk.Button(block2, text="⚙", command=self.callbacks['tg_sync'], bootstyle="info", width=5) btn_sync.pack(side="left", padx=3) ToolTip(btn_sync, text="Настройка рассылок TG", bootstyle="inverse-info") # Статистика TG за сегодня btn_stats = ttk.Button(block2, text="📈", command=self.callbacks['tg_stats'], bootstyle="info", width=5) btn_stats.pack(side="left", padx=3) ToolTip(btn_stats, text="Статистика TG за сегодня", bootstyle="inverse-info") ttk.Separator(btn_frame, orient="vertical").pack(side="left", fill="y", padx=10) # --- БЛОК 3: EXPORT и СОТРУДНИКИ --- block3 = ttk.Frame(btn_frame) block3.pack(side="left") btn_export = ttk.Button(block3, text="📗", command=self.callbacks['export'], bootstyle="success", width=5) btn_export.pack(side="left", padx=3) ToolTip(btn_export, text="Экспорт в Excel", bootstyle="inverse-success") btn_emp = ttk.Button(block3, text="👥", command=self.callbacks['open_emp'], bootstyle="secondary", width=5) btn_emp.pack(side="left", padx=3) ToolTip(btn_emp, text="Управление сотрудниками", bootstyle="inverse-secondary") # --- ФИЛЬТРЫ --- filter_frame = ttk.Labelframe(self, text="Фильтры", padding=5, bootstyle="secondary") filter_frame.pack(fill="x", padx=10, pady=5) ttk.Label(filter_frame, text="Срок:").pack(side="left", padx=5) self.filter_date = ttk.Combobox(filter_frame, values=DateFilter.list_all(), state="readonly", width=12) self.filter_date.current(0); self.filter_date.pack(side="left", padx=5) self.filter_date.bind("<<ComboboxSelected>>", lambda e: self.callbacks['apply_filter']()) ttk.Label(filter_frame, text="Статус:").pack(side="left", padx=5) self.filter_status = ttk.Combobox(filter_frame, values=["Все"] + TaskStatus.list_all(), state="readonly", width=22) self.filter_status.current(0); self.filter_status.pack(side="left", padx=5) self.filter_status.bind("<<ComboboxSelected>>", lambda e: self.callbacks['apply_filter']()) ttk.Label(filter_frame, text="Приоритет:").pack(side="left", padx=5) self.filter_prio = ttk.Combobox(filter_frame, values=["Все"] + Priority.list_all(), state="readonly", width=15) self.filter_prio.current(0) self.filter_prio.pack(side="left", padx=5) self.filter_prio.bind("<<ComboboxSelected>>", lambda e: self.callbacks['apply_filter']()) ttk.Label(filter_frame, text="Ответственный:").pack(side="left", padx=5) self.filter_assignee = ttk.Combobox(filter_frame, values=["Все"], width=15) self.filter_assignee.current(0) self.filter_assignee.pack(side="left", padx=5) self.filter_assignee.bind("<<ComboboxSelected>>", lambda e: self.callbacks['apply_filter']()) ttk.Label(filter_frame, text="🔎").pack(side="left", padx=(10, 2)) self.filter_search = ttk.Entry(filter_frame, width=20) self.filter_search.pack(side="left", padx=5) self.filter_search.bind("<KeyRelease>", lambda e: self.callbacks['apply_filter']()) ttk.Button(filter_frame, text="❌ Сброс", command=self.callbacks['reset_filter'], bootstyle="link").pack(side="left", padx=10) # --- ТАБЛИЦА --- cols = ("title", "prio", "assignee", "date", "poll_sent", "status", "poll_answered", "days", "comment") self.tree = ttk.Treeview(self, columns=cols, selectmode="browse", bootstyle="dark") def sort_cmd(col): return lambda: self.callbacks['sort'](col) self.tree.heading("#0", text="№", command=sort_cmd("#0")); self.tree.column("#0", width=80, anchor="w") self.tree.heading("title", text="Задача (Структура)", command=sort_cmd("title")); self.tree.column("title", width=350, anchor="w") self.tree.heading("prio", text="Приоритет", command=sort_cmd("prio")); self.tree.column("prio", width=100, anchor="center") self.tree.heading("assignee", text="Ответственный", command=sort_cmd("assignee")); self.tree.column("assignee", width=150, anchor="center") self.tree.heading("date", text="Срок", command=sort_cmd("date")); self.tree.column("date", width=100, anchor="center") self.tree.heading("poll_sent", text="Отправка"); self.tree.column("poll_sent", width=120, anchor="center") self.tree.heading("status", text="Статус", command=sort_cmd("status")); self.tree.column("status", width=180, anchor="center") self.tree.heading("poll_answered", text="Ответ"); self.tree.column("poll_answered", width=120, anchor="center") self.tree.heading("days", text="Дни"); self.tree.column("days", width=50, anchor="center") self.tree.heading("comment", text="Комментарий", command=sort_cmd("comment")) self.tree.pack(fill="both", expand=True, padx=10, pady=5) self.tree.tag_configure('completed', foreground='#AAAAAA') self.tree.tag_configure('overdue', foreground='#FF4444') self.tree.tag_configure('today', foreground='#FFD700') self.tree.bind("<ButtonPress-1>", self.on_drag_start); self.tree.bind("<B1-Motion>", self.on_drag_motion); self.tree.bind("<ButtonRelease-1>", self.on_drag_release) # --- DRAG AND DROP --- def on_drag_start(self, event): item = self.tree.identify_row(event.y) if item: self.dragged_item = item def on_drag_motion(self, event): if self.dragged_item: target = self.tree.identify_row(event.y) if target: self.tree.selection_set(target) def on_drag_release(self, event): if not self.dragged_item: return target_item = self.tree.identify_row(event.y) source_item = self.dragged_item self.dragged_item = None if not target_item or target_item == source_item: return if self.is_descendant(source_item, target_item): return self.callbacks['move_task'](source_item, target_item) def is_descendant(self, parent, candidate): children = self.tree.get_children(parent) for child in children: if child == candidate or self.is_descendant(child, candidate): return True return False def setup_context_menu(self): self.context_menu = tk.Menu(self, tearoff=0) self.context_menu.add_command(label="↳ Подзадача", command=self.callbacks['add_sub']) self.context_menu.add_command(label="💬 Комментарий", command=self.callbacks['comment']) self.context_menu.add_separator() self.context_menu.add_command(label="📅 +1 Рабочий день", command=self.callbacks['shift_date']) self.context_menu.add_separator() status_menu = tk.Menu(self.context_menu, tearoff=0) status_menu.add_command(label=TaskStatus.IN_PROGRESS, command=lambda: self.callbacks['set_status'](TaskStatus.IN_PROGRESS)) status_menu.add_command(label=TaskStatus.REVIEW, command=lambda: self.callbacks['set_status'](TaskStatus.REVIEW)) status_menu.add_command(label=TaskStatus.DISCUSS, command=lambda: self.callbacks['set_status'](TaskStatus.DISCUSS)) status_menu.add_command(label=TaskStatus.DONE, command=lambda: self.callbacks['set_status'](TaskStatus.DONE)) self.context_menu.add_cascade(label="🏁 Изменить статус", menu=status_menu) self.context_menu.add_separator() self.context_menu.add_command(label="✏ Редактировать", command=self.callbacks['edit']) self.context_menu.add_command(label="💾 Сохранить", command=self.callbacks['save_edit']) self.context_menu.add_command(label="🗑 Удалить", command=self.callbacks['delete']) self.tree.bind("<Button-3>", self.on_right_click) def on_right_click(self, event): item = self.tree.identify_row(event.y) if item: self.tree.selection_set(item) self.tree.focus(item) self.context_menu.post(event.x_root, event.y_root) def apply_shortcuts(self, widget): pass def get_input_data(self): txt = self.entry_task.get("1.0", "end-1c").strip() if not txt: return None return (txt, self.combo_priority.get(), self.combo_assignee.get(), self.entry_date.entry.get()) def clear_inputs(self): self.entry_task.delete("1.0", tk.END) def set_inputs(self, title, prio, assignee, date): self.entry_task.delete("1.0", tk.END); self.entry_task.insert("1.0", title) self.combo_priority.set(prio); self.combo_assignee.set(assignee) self.entry_date.entry.delete(0, tk.END); self.entry_date.entry.insert(0, date) def get_expanded_items(self): expanded = set() def _recurse(item): if self.tree.item(item, 'open'): expanded.add(item) for child in self.tree.get_children(item): _recurse(child) for root in self.tree.get_children(): _recurse(root) return expanded