/
AxeOn
/
ProjectAi
Обзор
Документация
Войти
/
AxeOn
/
ProjectAi
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
dashboard_module.py
673 строки
26 KB
Дмитрий
refactor: complete dataclass implementation for DB layer and update call sites
25 апр 2026, 16:49
25 апр 2026, 16:49
111b3f6
Код
Авторство
О чём код?
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ dashboard_module.py - Главный дашборд Офис-Планировщика Показывает ключевые метрики и статус в реальном времени: - Задачи (выполнение, просрочки) - Загрузка сотрудников - События сегодня - Финансовые показатели """ import logging import ttkbootstrap as ttk from ttkbootstrap.constants import * import tkinter as tk from datetime import datetime, date, timedelta from collections import defaultdict from tkinter import messagebox logger = logging.getLogger(__name__) class DashboardWidget(ttk.Frame): """Базовый класс для виджета дашборда""" def __init__(self, parent, title, bootstyle="primary"): super().__init__(parent, bootstyle=f"{bootstyle}") # Заголовок header = ttk.Frame(self, bootstyle=f"{bootstyle}") header.pack(fill=X, padx=10, pady=(10, 5)) ttk.Label(header, text=title, font=("Arial", 12, "bold"), bootstyle=f"{bootstyle}-inverse").pack(side=LEFT) # Кнопка обновления self.refresh_btn = ttk.Button(header, text="🔄", width=3, command=self.refresh, bootstyle=f"{bootstyle}-outline") self.refresh_btn.pack(side=RIGHT) # Контент self.content = ttk.Frame(self) self.content.pack(fill=BOTH, expand=True, padx=10, pady=10) def refresh(self): """Переопределяется в дочерних классах""" pass class TasksWidget(DashboardWidget): """Виджет: Задачи сегодня""" def __init__(self, parent, db): super().__init__(parent, "📋 Задачи сегодня", "primary") self.db = db self.setup_ui() self.refresh() def setup_ui(self): # Большие цифры stats_frame = ttk.Frame(self.content) stats_frame.pack(fill=X, pady=10) self.total_label = ttk.Label(stats_frame, text="0", font=("Arial", 36, "bold"), bootstyle="primary") self.total_label.pack() ttk.Label(stats_frame, text="всего задач", font=("Arial", 10)).pack() # Прогресс-бар self.progress = ttk.Progressbar(self.content, length=300, mode='determinate', bootstyle="success-striped") self.progress.pack(pady=10) self.progress_label = ttk.Label(self.content, text="0% выполнено", font=("Arial", 10)) self.progress_label.pack() # Детали details_frame = ttk.Frame(self.content) details_frame.pack(fill=X, pady=10) # Три колонки for col, (icon, text, style) in enumerate([ ("✅", "Завершено", "success"), ("⏳", "В работе", "warning"), ("🔥", "Просрочено", "danger") ]): frame = ttk.Frame(details_frame) frame.pack(side=LEFT, expand=True, fill=X, padx=5) label = ttk.Label(frame, text=f"{icon} 0", font=("Arial", 14, "bold"), bootstyle=style) label.pack() setattr(self, f"{text.lower().split()[0]}_label", label) ttk.Label(frame, text=text, font=("Arial", 8)).pack() def refresh(self): try: today = date.today().isoformat() # Получаем задачи all_tasks = self.db.get_all_tasks() total = len(all_tasks) completed = sum(1 for t in all_tasks if 'Выполнено' in t.get('status', '')) in_progress = sum(1 for t in all_tasks if 'работе' in t.get('status', '')) # Просроченные overdue = 0 for t in all_tasks: if 'Выполнено' in t.get('status', ''): continue try: due_date = datetime.strptime(t.get('due_date', ''), '%Y-%m-%d').date() if due_date < date.today(): overdue += 1 except Exception: pass # Обновляем UI self.total_label.config(text=str(total)) if total > 0: percent = int((completed / total) * 100) self.progress['value'] = percent self.progress_label.config(text=f"{percent}% выполнено") else: self.progress['value'] = 0 self.progress_label.config(text="Нет задач") self.завершено_label.config(text=f"✅ {completed}") self.в_label.config(text=f"⏳ {in_progress}") self.просрочено_label.config(text=f"🔥 {overdue}") except Exception as e: logger.error(f"Error refreshing tasks widget: {e}") class EmployeesWidget(DashboardWidget): """Виджет: Загрузка сотрудников""" def __init__(self, parent, db): super().__init__(parent, "👥 Сотрудники сегодня", "info") self.db = db self.setup_ui() self.refresh() def setup_ui(self): # Статусы сотрудников self.status_frame = ttk.Frame(self.content) self.status_frame.pack(fill=BOTH, expand=True) # Создаём метки для статусов for status, icon, color in [ ("working", "💼 Работают", "success"), ("vacation", "🏖️ В отпуске", "warning"), ("absent", "❌ Отсутствуют", "danger") ]: frame = ttk.Frame(self.status_frame) frame.pack(fill=X, pady=5) label = ttk.Label(frame, text=f"{icon}: 0", font=("Arial", 12), bootstyle=color) label.pack(side=LEFT) setattr(self, f"{status}_label", label) # Прогресс-бар progress = ttk.Progressbar(frame, length=200, mode='determinate', bootstyle=color) progress.pack(side=LEFT, padx=10) setattr(self, f"{status}_progress", progress) def refresh(self): try: today = date.today().isoformat() # Получаем сотрудников employees = self.db.get_employees() total = len(employees) if total == 0: return working = 0 vacation = 0 absent = 0 for emp in employees: emp_id = emp['id'] # Проверяем календарь calendar = self.db.get_employee_calendar(emp_id, date.today().year, date.today().month) today_record = next((c for c in calendar if c['date'] == today), None) if today_record: if today_record.get('is_vacation'): vacation += 1 elif today_record.get('is_working'): if today_record.get('is_absent'): absent += 1 else: working += 1 else: # Выходной pass else: # Нет данных - считаем работает working += 1 # Обновляем UI self.working_label.config(text=f"💼 Работают: {working}") self.working_progress['value'] = (working / total) * 100 self.vacation_label.config(text=f"🏖️ В отпуске: {vacation}") self.vacation_progress['value'] = (vacation / total) * 100 self.absent_label.config(text=f"❌ Отсутствуют: {absent}") self.absent_progress['value'] = (absent / total) * 100 except Exception as e: logger.error(f"Error refreshing employees widget: {e}") class EventsWidget(DashboardWidget): """Виджет: События сегодня""" def __init__(self, parent, db): super().__init__(parent, "📅 События", "warning") self.db = db self.setup_ui() self.refresh() def setup_ui(self): # Список событий self.events_text = tk.Text(self.content, height=8, width=40, font=("Arial", 10), bg="#2b2b2b", fg="white", relief=FLAT) self.events_text.pack(fill=BOTH, expand=True) def refresh(self): try: self.events_text.delete("1.0", END) today = date.today() events = [] # Дни рождения employees = self.db.get_employees() for emp in employees: # Проверяем есть ли дата рождения (нужно добавить в БД) # birth_date = emp.get('birth_date') # if birth_date: # ... pass # Начало отпусков vacations = self.db.get_vacations() for vac in vacations: try: start = datetime.strptime(vac['start_date'], '%Y-%m-%d').date() end = datetime.strptime(vac['end_date'], '%Y-%m-%d').date() if start == today: events.append(f"🏖️ {vac['employee_name']} уходит в отпуск") elif end == today: events.append(f"💼 {vac['employee_name']} выходит из отпуска") except Exception: pass # Просроченные задачи tasks = self.db.get_all_tasks() overdue_count = 0 for t in tasks: if 'Выполнено' in t.get('status', ''): continue try: due = datetime.strptime(t.get('due_date', ''), '%Y-%m-%d').date() if due < today: overdue_count += 1 except Exception: pass if overdue_count > 0: events.append(f"⚠️ {overdue_count} просроченных задач") # Отображаем события if events: for event in events: self.events_text.insert(END, f"• {event}\n") else: self.events_text.insert(END, "✨ Нет событий на сегодня") except Exception as e: logger.error(f"Error refreshing events widget: {e}") self.events_text.insert(END, f"Ошибка: {e}") class FinanceWidget(DashboardWidget): """Виджет: Финансовые показатели""" def __init__(self, parent, db): super().__init__(parent, "💰 Финансы (текущий месяц)", "success") self.db = db self.setup_ui() self.refresh() def setup_ui(self): # Выручка revenue_frame = ttk.Frame(self.content) revenue_frame.pack(fill=X, pady=10) ttk.Label(revenue_frame, text="Выручка:", font=("Arial", 10)).pack(side=LEFT) self.revenue_label = ttk.Label(revenue_frame, text="0 ₽", font=("Arial", 16, "bold"), bootstyle="success") self.revenue_label.pack(side=RIGHT) # Зарплата salary_frame = ttk.Frame(self.content) salary_frame.pack(fill=X, pady=10) ttk.Label(salary_frame, text="Зарплата:", font=("Arial", 10)).pack(side=LEFT) self.salary_label = ttk.Label(salary_frame, text="0 ₽", font=("Arial", 16, "bold"), bootstyle="warning") self.salary_label.pack(side=RIGHT) # Прибыль profit_frame = ttk.Frame(self.content) profit_frame.pack(fill=X, pady=10) ttk.Label(profit_frame, text="Прибыль:", font=("Arial", 10)).pack(side=LEFT) self.profit_label = ttk.Label(profit_frame, text="0 ₽", font=("Arial", 16, "bold"), bootstyle="info") self.profit_label.pack(side=RIGHT) def refresh(self): try: # Получаем данные за текущий месяц today = date.today() period = f"{today.year}-{today.month:02d}" # Используем импорт из salary_module try: from salary_module import calculate_monthly_salary_data data = calculate_monthly_salary_data(self.db, str(today.year), f"{today.month:02d}") total_revenue = sum(emp.get('total_revenue', 0) for emp in data.values()) total_salary = sum(emp.get('total_net', 0) for emp in data.values()) total_profit = sum(emp.get('total_profit', 0) for emp in data.values()) self.revenue_label.config(text=f"{total_revenue:,.0f} ₽") self.salary_label.config(text=f"{total_salary:,.0f} ₽") self.profit_label.config(text=f"{total_profit:,.0f} ₽") except ImportError: self.revenue_label.config(text="N/A") self.salary_label.config(text="N/A") self.profit_label.config(text="N/A") except Exception as e: logger.error(f"Error refreshing finance widget: {e}") class NotificationsWidget(DashboardWidget): """Виджет: Последние уведомления""" def __init__(self, parent, db): super().__init__(parent, "🔔 Уведомления", "secondary") self.db = db self.setup_ui() self.refresh() def setup_ui(self): # Список уведомлений self.notif_text = tk.Text(self.content, height=8, width=40, font=("Arial", 9), bg="#2b2b2b", fg="white", relief=FLAT) self.notif_text.pack(fill=BOTH, expand=True) def refresh(self): try: self.notif_text.delete("1.0", END) # Получаем последние события Telegram with self.db.pool.get_connection() as conn: cursor = conn.cursor() cursor.execute(""" SELECT event_type, created_at FROM tg_stats ORDER BY created_at DESC LIMIT 10 """) events = cursor.fetchall() if events: for event in events: event_type = event[0] or "событие" created = event[1] or "" # Форматируем время try: dt = datetime.fromisoformat(created) time_str = dt.strftime("%H:%M") except Exception: time_str = "" self.notif_text.insert(END, f"[{time_str}] {event_type}\n") else: self.notif_text.insert(END, "Нет уведомлений") except Exception as e: logger.error(f"Error refreshing notifications widget: {e}") self.notif_text.insert(END, f"Ошибка: {e}") class DeadlinesWidget(DashboardWidget): """Виджет контроля сроков для главного дашборда.""" def __init__(self, parent, db): super().__init__(parent, "⏰ Контроль сроков", bootstyle="danger") self.db = db self._threshold = int(db.get_setting('deadline_threshold', '3') or 3) content = ttk.Frame(self, bootstyle="danger") content.pack(fill=BOTH, expand=True, padx=10, pady=6) # ── Счётчики ───────────────────────────────────────────────────────── counters = ttk.Frame(content) counters.pack(fill=X) for col, (lbl, attr, style) in enumerate([ ("Всего дел", '_lbl_total', 'danger'), ("Срок ≥ порога", '_lbl_over', 'warning'), ("Без эксперта", '_lbl_unassigned', 'secondary'), ]): f = ttk.Frame(counters, bootstyle=style) f.grid(row=0, column=col, padx=4, sticky='nsew') counters.columnconfigure(col, weight=1) ttk.Label(f, text=lbl, font=("Arial", 8), bootstyle=f"inverse-{style}").pack(pady=(4, 0)) lbl_w = ttk.Label(f, text="—", font=("Arial", 18, "bold"), bootstyle=f"inverse-{style}") lbl_w.pack() setattr(self, attr, lbl_w) # ── Мини-график (Canvas) ────────────────────────────────────────────── ttk.Separator(content, orient=HORIZONTAL).pack(fill=X, pady=6) ttk.Label(content, text="Дел со сроком ≥ порога по датам загрузки:", font=("Arial", 8)).pack(anchor='w') self._canvas = tk.Canvas(content, height=80, bg='#2b2b2b', highlightthickness=0) self._canvas.pack(fill=X, pady=(2, 4)) self._canvas.bind('<Configure>', lambda e: self._draw_chart()) self._trend_data = [] self.refresh() def refresh(self): try: self._threshold = int( self.db.get_setting('deadline_threshold', '3') or 3) cases = self.db.get_deadline_cases() # последняя дата total = len(cases) over = sum(1 for c in cases if (c.deadline or 0) >= self._threshold) unasgn = sum(1 for c in cases if (c.deadline or 0) >= self._threshold and not (c.expert_calculation or '').strip().replace('nan', '')) self._lbl_total.config(text=str(total)) self._lbl_over.config(text=str(over)) self._lbl_unassigned.config(text=str(unasgn)) self._trend_data = self.db.get_deadline_trend(self._threshold) self._draw_chart() except Exception as e: logger.error(f"DeadlinesWidget.refresh: {e}") def _draw_chart(self): """Рисуем мини-линейный график на Canvas.""" self._canvas.delete('all') data = self._trend_data if not data: self._canvas.create_text( 10, 40, text="Нет данных", fill='#888', anchor='w') return w = self._canvas.winfo_width() or 300 h = self._canvas.winfo_height() or 80 pad_l, pad_r, pad_t, pad_b = 4, 4, 8, 18 vals = [r['over_threshold'] for r in data] max_v = max(vals) if max(vals) > 0 else 1 n = len(vals) def _x(i): return pad_l + i * (w - pad_l - pad_r) / max(n - 1, 1) def _y(v): return pad_t + (1 - v / max_v) * (h - pad_t - pad_b) # Заливка под кривой poly_pts = [pad_l, h - pad_b] for i, v in enumerate(vals): poly_pts += [_x(i), _y(v)] poly_pts += [_x(n - 1), h - pad_b] self._canvas.create_polygon(poly_pts, fill='#4a1010', outline='') # Линия pts = [] for i, v in enumerate(vals): pts += [_x(i), _y(v)] if len(pts) >= 4: self._canvas.create_line(pts, fill='#e57373', width=2, smooth=True) # Точки + подписи дат (последняя) for i, row in enumerate(data): x, y = _x(i), _y(vals[i]) self._canvas.create_oval(x-3, y-3, x+3, y+3, fill='#ef9a9a', outline='') if data: last = data[-1] self._canvas.create_text( _x(n-1), h - 2, text=last['load_date'][5:], # MM-DD fill='#aaa', font=('Arial', 7), anchor='s') self._canvas.create_text( _x(n-1), _y(vals[-1]) - 6, text=str(vals[-1]), fill='#ef9a9a', font=('Arial', 7, 'bold'), anchor='s') class DashboardModule(ttk.Frame): """Главный модуль дашборда""" def __init__(self, parent, db_manager): super().__init__(parent) self.pack(fill=BOTH, expand=True) self.db = db_manager self.setup_ui() def setup_ui(self): # Заголовок header = ttk.Frame(self, bootstyle="dark") header.pack(fill=X, padx=10, pady=10) ttk.Label(header, text="📊 Главный Дашборд", font=("Arial", 18, "bold"), bootstyle="inverse-dark").pack(side=LEFT) # Время обновления self.update_label = ttk.Label(header, text=f"Обновлено: {datetime.now().strftime('%H:%M:%S')}", font=("Arial", 9), bootstyle="inverse-dark") self.update_label.pack(side=RIGHT, padx=10) # Кнопка обновить всё ttk.Button(header, text="🔄 Обновить всё", command=self.refresh_all, bootstyle="success").pack(side=RIGHT) ttk.Separator(self, orient=HORIZONTAL).pack(fill=X, pady=5) # Основной контент с прокруткой canvas = tk.Canvas(self, bg='#2b2b2b', highlightthickness=0) scrollbar = ttk.Scrollbar(self, orient=VERTICAL, command=canvas.yview) scrollable_frame = ttk.Frame(canvas) scrollable_frame.bind( "<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) canvas.pack(side=LEFT, fill=BOTH, expand=True) scrollbar.pack(side=RIGHT, fill=Y) # Создаём сетку виджетов (2x3) self.widgets = [] # Строка 1 row1 = ttk.Frame(scrollable_frame) row1.pack(fill=X, padx=10, pady=5) # Задачи (большой) tasks_widget = TasksWidget(row1, self.db) tasks_widget.pack(side=LEFT, fill=BOTH, expand=True, padx=5) self.widgets.append(tasks_widget) # События events_widget = EventsWidget(row1, self.db) events_widget.pack(side=LEFT, fill=BOTH, expand=True, padx=5) self.widgets.append(events_widget) # Строка 2 row2 = ttk.Frame(scrollable_frame) row2.pack(fill=X, padx=10, pady=5) # Сотрудники employees_widget = EmployeesWidget(row2, self.db) employees_widget.pack(side=LEFT, fill=BOTH, expand=True, padx=5) self.widgets.append(employees_widget) # Финансы finance_widget = FinanceWidget(row2, self.db) finance_widget.pack(side=LEFT, fill=BOTH, expand=True, padx=5) self.widgets.append(finance_widget) # Строка 3 row3 = ttk.Frame(scrollable_frame) row3.pack(fill=X, padx=10, pady=5) # Контроль сроков deadlines_widget = DeadlinesWidget(row3, self.db) deadlines_widget.pack(side=LEFT, fill=BOTH, expand=True, padx=5) self.widgets.append(deadlines_widget) # Уведомления notif_widget = NotificationsWidget(row3, self.db) notif_widget.pack(side=LEFT, fill=BOTH, expand=True, padx=5) self.widgets.append(notif_widget) # Автообновление каждые 60 секунд self.auto_refresh() def refresh_all(self): """Обновить все виджеты""" for widget in self.widgets: widget.refresh() # Обновляем время self.update_label.config( text=f"Обновлено: {datetime.now().strftime('%H:%M:%S')}" ) def auto_refresh(self): """Автообновление каждые 60 секунд""" self.refresh_all() self.after(60000, self.auto_refresh) # 60 секунд # === ИНТЕГРАЦИЯ В ГЛАВНОЕ ОКНО === if __name__ == "__main__": pass