Загрузка данных
# -*- coding: utf-8 -*-
# Файл: modules/project_event.py
import os
import sqlite3
import tkinter as tk
from tkinter import Button, Radiobutton, Toplevel, messagebox, ttk
from tkinter import messagebox
from modules.project_database import TABLE_PROJECT_DATA
class ProjectEventWindow:
"""
Окно для работы с событиями: ввод текста, добавление в список,
управление записями и печать.
Принимает данные (код проекта) из окна выбора параметров.
"""
def __init__(self, manager, selected_data, root_window=None):
"""
:param selected_data: Словарь с данными из ProjectListWindow.
Должен содержать 'project_code', 'group_code', 'indicator_name', 'indicator_code'.
"""
self.manager = manager
self.root_window = root_window
# --- ВАЛИДАЦИЯ ВХОДНЫХ ДАННЫХ ---
project_code_from_data = selected_data.get("project_code")
group_code_from_data = selected_data.get("group_code")
indicator_name_from_data = selected_data.get("indicator_name")
indicator_code_from_data = selected_data.get("indicator_code")
if not all(
[
project_code_from_data,
group_code_from_data,
indicator_name_from_data,
indicator_code_from_data,
]
):
messagebox.showerror(
"Ошибка данных",
"Не удалось получить данные проекта, группы или показателя.",
)
return
self.selected_data = selected_data
self.project_code = project_code_from_data
self.group_code = group_code_from_data
self.indicator_name = indicator_name_from_data
self.indicator_code = indicator_code_from_data
# --- СОЗДАНИЕ ОКНА ---
# Используем Toplevel, если есть родительское окно, иначе создаем
# главное
if root_window:
self.window = Toplevel(root_window)
else:
self.window = tk.Tk()
project_name = self.selected_data.get("project_name", "Неизвестный проект")
self.window.title(f"Ввод событий - {project_name}")
self.window.geometry("750x600")
# --- КОРРЕКТНОЕ ЗАКРЫТИЕ ОКНА ---
# Эта строка связывает системную кнопку закрытия (крестик) с нашим
# методом
self.window.protocol("WM_DELETE_WINDOW", self.on_close)
# Создаем виджеты после того, как окно успешно создано и настроено
self._create_widgets()
def _create_widgets(self):
main_frame = tk.Frame(self.window)
main_frame.pack(padx=10, pady=2, fill="both", expand=True)
# --- Блок информации о контексте (Группа и Показатель) ---
info_frame = tk.Frame(main_frame)
info_frame.pack(fill="x", pady=5, padx=15)
context_text = f"Группа: {self.group_code}, Показатель: {self.indicator_name}"
tk.Label(info_frame, text=context_text, font=("Arial", 10)).pack(anchor="w")
# --- Блок ввода данных ---
date_frame = tk.Frame(main_frame)
date_frame.pack(fill="x", pady=3)
tk.Label(date_frame, text="Дата события:").pack(side="left")
self.date_entry = tk.Entry(date_frame, width=12)
self.date_entry.insert(0, "2026-05-08")
self.date_entry.pack(side="left", padx=(5, 0), ipadx=5)
# Радиокнопки "Направленность"
direction_frame = tk.Frame(main_frame)
direction_frame.pack(fill="x", pady=3, anchor="w")
tk.Label(direction_frame, text="Направленность:").pack(side="left")
radio_frame = tk.Frame(direction_frame)
radio_frame.pack(side="left", padx=(5, 0))
self.direction_var = tk.IntVar(value=1)
Radiobutton(
radio_frame, text="+ К цели", variable=self.direction_var, value=1
).pack(side="left", padx=(0, 10))
Radiobutton(
radio_frame, text="- От цели", variable=self.direction_var, value=-1
).pack(side="left", padx=(0, 10))
# Поле "Событие"
tk.Label(main_frame, text="Событие:").pack(anchor="w")
self.event_text = tk.Text(main_frame, height=4, wrap="word")
self.event_text.pack(fill="x", pady=(2, 5), padx=15)
# --- Блок кнопок управления ---
ctrl_btns = tk.Frame(main_frame)
ctrl_btns.pack(fill="x", pady=(0, 5), padx=15)
Button(
ctrl_btns, text="Вставить", command=self.paste_from_clipboard, width=15
).pack(side="left", padx=(0, 10))
Button(ctrl_btns, text="Добавить", width=15, command=self.add_entry).pack(
side="left", padx=(10, 10)
)
# Кнопка "Удалить"
Button(
ctrl_btns, text="Удалить", width=15, command=self.delete_selected_event
).pack(side="left", padx=(10, 10))
# Кнопка "Печать в Word" (пока заглушка)
Button(
ctrl_btns, text="Печать в Word", width=15, command=self.print_to_word
).pack(side="left", padx=(10, 10))
# --- Блок списка событий (ТАБЛИЦА) ---
events_main_frame = tk.Frame(main_frame)
events_main_frame.pack(fill="both", expand=True, pady=(5, 0), padx=15)
tk.Label(events_main_frame, text="Список событий:").pack(anchor="w")
# 1. Определяем колонки
columns = ("code", "date", "direction", "event")
# 2. Создаем Treeview (таблицу)
self.events_tree = ttk.Treeview(
events_main_frame, columns=columns, show="headings", height=10
)
# --- НОВАЯ СТРОКА: Привязка двойного клика ---
# Привязываем событие к конкретным элементам (строкам) таблицы.
# 'i' означает item (элемент/строка).
self.events_tree.bind("<Double-1>", self.on_edit_event)
# 3. Задаем ширину и заголовки (РЕШЕНИЕ ПРОБЛЕМЫ РАЗМЕРОВ)
self.events_tree.column("code", width=80, anchor="center", minwidth=60)
self.events_tree.column("date", width=90, anchor="center", minwidth=70)
self.events_tree.column("direction", width=45, anchor="center", minwidth=30)
self.events_tree.column("event", anchor="w", minwidth=200, stretch=tk.YES)
self.events_tree.heading("code", text="Код")
self.events_tree.heading("date", text="Дата")
self.events_tree.heading("direction", text="+/-")
self.events_tree.heading("event", text="Событие")
# 4. Добавляем скроллбар и связываем его с таблицей
scrollbar = tk.Scrollbar(
events_main_frame, orient="vertical", command=self.events_tree.yview
)
self.events_tree.configure(yscrollcommand=scrollbar.set)
# 5. Размещаем таблицу и скроллбар на экране
self.events_tree.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# --- Блок поиска по коду ---
code_search_frame = tk.Frame(main_frame)
code_search_frame.pack(fill="x", pady=(5, 0), padx=15)
tk.Label(code_search_frame, text="Код события:").grid(
row=0, column=0, sticky="e", padx=5
)
self.code_entry = tk.Entry(code_search_frame, width=15)
self.code_entry.grid(row=0, column=1, sticky="we", padx=5, pady=5)
search_btn = Button(
code_search_frame, text="Поиск", command=self.search_by_code
)
search_btn.grid(row=0, column=2, sticky="we", padx=5, pady=5)
def add_entry(self):
"""Обработка нажатия кнопки 'Добавить'."""
# 1. Получаем данные из полей ввода
event_text_content = self.event_text.get("1.0", tk.END).strip()
date_event = self.date_entry.get()
direction = self.direction_var.get()
# Проверяем, что поле "Событие" не пустое
if not event_text_content:
messagebox.showwarning("Ошибка", "Поле 'Событие' пустое.")
return
# 2. Подготавливаем путь к базе данных
db_folder = os.path.join(os.getcwd(), "project_dbs")
db_path = os.path.join(db_folder, f"{self.project_code}.db")
try:
# Создаем папку, если ее нет, и подключаемся к БД
os.makedirs(db_folder, exist_ok=True)
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Проверяем и исправляем названия колонок, если нужно (для
# совместимости)
cursor.execute(f"PRAGMA table_info({TABLE_PROJECT_DATA})")
columns = [column[1] for column in cursor.fetchall()]
data_column = "event_data" if "event_data" in columns else "ivent_data"
code_column = "event_code" if "event_code" in columns else "ivent_code"
sign_column = "event_sign" if "event_sign" in columns else "ivent_sign"
# 3. Генерируем новый код события
cursor.execute(
f"""
SELECT COUNT(*)
FROM {TABLE_PROJECT_DATA}
WHERE project_code = ? AND indicator_code = ?
""",
(self.project_code, self.indicator_code),
)
count = cursor.fetchone()[0]
event_sequence_number = count + 1
event_code = f"{self.indicator_code}_{event_sequence_number}"
# 4. Вставляем данные в базу
cursor.execute(
f"""
INSERT INTO {TABLE_PROJECT_DATA} (
project_code, group_code, indicator_name, indicator_code,
{data_column}, {sign_column}, event_text_content, {code_column}, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
""",
(
self.project_code,
self.group_code,
self.indicator_name,
self.indicator_code,
date_event,
direction,
event_text_content,
event_code,
),
)
# 5. Сохраняем изменения в файл базы данных
conn.commit()
# 6. Обновляем интерфейс (ТАБЛИЦУ), если запись прошла успешно
if conn.total_changes > 0:
# Определяем текст для колонки "Направленность"
direction_str = "+" if direction == 1 else "-"
# Вставляем новую строку в таблицу Treeview
self.events_tree.insert(
"",
"end",
values=(
event_code, # Код
date_event, # Дата
direction_str, # Направленность
event_text_content, # Текст события
),
)
# Очищаем поле ввода текста
self.event_text.delete("1.0", tk.END)
# Показываем сообщение об успехе
messagebox.showinfo(
"Успех", f"Данные добавлены.\nКод события: {event_code}"
)
# Делаем окно "всегда поверх", чтобы оно не уходило на задний
# план
self.window.attributes("-topmost", 1)
self.window.after(100, lambda: self.window.attributes("-topmost", 0))
except Exception as e:
messagebox.showerror("Ошибка БД", f"Не удалось записать данные: {e}")
finally:
# Гарантируем, что соединение с базой данных будет закрыто
if "conn" in locals():
conn.close()
def load_events(self):
"""Загружает события из базы данных и отображает их в таблице."""
try:
for item in self.events_tree.get_children():
self.events_tree.delete(item)
db_path = os.path.join("project_dbs", f"{self.project_code}.db")
if not os.path.exists(db_path):
return
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute(
f"""
SELECT event_code, event_data, event_sign, event_text_content
FROM {TABLE_PROJECT_DATA}
WHERE project_code = ? AND group_code = ? AND indicator_name = ?
ORDER BY id DESC
""",
(self.project_code, self.group_code, self.indicator_name),
)
rows = cursor.fetchall()
if not rows:
return
for row in rows:
if row and len(row) >= 4:
event_code, event_date, sign_val, text_content = row
direction_str = "+" if sign_val == 1 else "-"
self.events_tree.insert(
"",
"end",
values=(event_code, event_date, direction_str, text_content),
)
except Exception as e:
pass # Или можно логировать ошибку в файл, если нужно
finally:
if "conn" in locals():
conn.close()
def paste_from_clipboard(self):
"""Вставляет текст из буфера обмена в поле события."""
try:
txt = self.window.clipboard_get()
self.event_text.insert(tk.INSERT, txt)
except Exception as e:
pass
def archive_project(self):
"""Обработка нажатия кнопки 'В архив'."""
messagebox.showinfo(
"Успех", f"Проект с кодом '{self.project_code}' архивирован."
)
def on_close(self):
"""Закрытие окна ввода событий и возврат в окно выбора."""
try:
# Если у нас есть родительское окно (окно выбора) и у него есть
# метод возврата
if self.root_window and hasattr(self.root_window, "show_selection_window"):
# Вызываем метод возврата в главном окне
self.root_window.show_selection_window()
# Затем уничтожаем текущее окно ввода событий
self.window.destroy()
except Exception as e:
print(f"[ДИАГНОСТИКА] Ошибка при закрытии окна: {e}")
def search_by_code(self):
"""
Выполняет поиск события по коду.
Если событие найдено:
1. Находит его группу и показатель.
2. Меняет текущую группу/показатель в окне.
3. Загружет список событий для новой группы.
4. Выделяет найденную строку.
"""
entered_code = self.code_entry.get().strip()
if not entered_code:
messagebox.showwarning("Поиск", "Введите код события.")
return
db_path = os.path.join("project_dbs", f"{self.project_code}.db")
if not os.path.exists(db_path):
messagebox.showerror("Ошибка", "База данных не найдена.")
return
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Запрос: ищем событие по коду и получаем его группу и показатель
cursor.execute(
f"""
SELECT group_code, indicator_name, indicator_code
FROM {TABLE_PROJECT_DATA}
WHERE project_code = ? AND event_code = ?
LIMIT 1
""",
(self.project_code, entered_code),
)
result = cursor.fetchone()
if not result:
messagebox.showinfo("Поиск", "Событие с таким кодом не найдено.")
return
# Получаем данные о том, где лежит это событие
found_group_code = result[0]
found_indicator_name = result[1]
found_indicator_code = result[2]
# Проверяем, не находимся ли мы уже в нужной группе/показателе
current_context = (self.group_code, self.indicator_name)
new_context = (found_group_code, found_indicator_name)
# Обновляем контекст окна ТОЛЬКО если он изменился
if current_context != new_context:
self.group_code = found_group_code
self.indicator_name = found_indicator_name
self.indicator_code = found_indicator_code
# Обновляем текст в информационном блоке вверху окна
context_text = (
f"Группа: {self.group_code}, Показатель: {self.indicator_name}"
)
# Находим виджет Label по его тексту в дочерних окнах и меняем текст
for child in self.window.winfo_children():
if isinstance(child, tk.Frame): # Ищем фрейм info_frame
for label in child.winfo_children():
if isinstance(label, tk.Label):
label.config(text=context_text)
break
# Перезагружаем список событий для новой группы/показателя,
# чтобы таблица обновилась
self.load_events()
# Теперь ищем строку с кодом в уже загруженной таблице
self._select_row_by_event_code(entered_code)
except Exception as e:
messagebox.showerror("Ошибка БД", f"Ошибка при поиске: {e}")
finally:
if "conn" in locals():
conn.close()
def _select_row_by_event_code(self, event_code_to_find):
"""
Вспомогательный метод.
Выделяет строку с указанным кодом в таблице и прокручивает к ней.
"""
children = self.events_tree.get_children()
if not children:
return
# Снимаем выделение со всех строк (если было)
self.events_tree.selection_remove(children)
# Ищем нужную строку по значению в первом столбце (Код)
for item in children:
code_val = self.events_tree.item(item, "values")[0]
if code_val == event_code_to_find:
# Выделяем строку (стандартный синий цвет Windows)
self.events_tree.selection_add(item)
# Прокручиваем таблицу так, чтобы строка стала видимой
self.events_tree.see(item)
# Устанавливаем фокус на таблицу (для стандартной подсветки)
self.events_tree.focus(item)
# Выходим из цикла, так как нашли нужное событие
break
def on_edit_event(self, event):
"""Обработчик двойного клика по строке в таблице."""
selected_item = self.events_tree.selection()
if not selected_item:
return
item_values = self.events_tree.item(selected_item[0], "values")
if not item_values:
return
event_code = item_values[0]
date_val = item_values[1]
direction_val = 1 if item_values[2] == "+" else -1
event_text = item_values[3]
# --- ИЗМЕНЕНИЕ ЗДЕСЬ ---
# Передаем 'self' как parent_window
edit_window = EventEditWindow(
self, # <-- Вот эта строчка! Передаем текущий объект как родителя
event_code,
date_val,
direction_val,
event_text,
)
def update_event_in_db(self, event_code, new_date, new_direction, new_event_text):
"""Обновляет событие в базе данных и таблице."""
db_path = os.path.join("project_dbs", f"{self.project_code}.db")
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute(
f"""
UPDATE {TABLE_PROJECT_DATA}
SET event_data = ?, event_sign = ?, event_text_content = ?
WHERE event_code = ? AND project_code = ? AND indicator_code = ?
""",
(
new_date,
new_direction,
new_event_text,
event_code,
self.project_code,
self.indicator_code,
),
)
conn.commit()
if conn.total_changes > 0:
selected_item = self.events_tree.selection()
if selected_item:
direction_str = "+" if new_direction == 1 else "-"
self.events_tree.item(
selected_item[0],
values=(event_code, new_date, direction_str, new_event_text),
)
messagebox.showinfo("Успех", "Изменения сохранены.")
else:
messagebox.showwarning("Нет изменений", "Данные не были изменены в БД.")
except Exception as e:
messagebox.showerror("Ошибка БД", f"Не удалось обновить данные: {e}")
finally:
if "conn" in locals():
conn.close()
def on_right_click(self, event):
"""Обработчик правого клика по строке в таблице."""
# Находим элемент под курсором
item = self.events_tree.identify_row(event.y)
if not item:
return
# Выделяем строку (если не выделена)
if item not in self.events_tree.selection():
self.events_tree.selection_set(item)
# Создаём контекстное меню с пунктом "Удалить"
menu = tk.Menu(self.window, tearoff=0)
menu.add_command(label="Удалить", command=lambda: self.delete_selected_event())
try:
menu.tk_popup(event.x_root, event.y_root)
finally:
menu.grab_release()
def delete_selected_event(self):
"""Удаляет выбранное событие из таблицы и базы данных."""
selected_items = self.events_tree.selection()
if not selected_items:
return
item_values = self.events_tree.item(selected_items[0], "values")
if not item_values:
return
event_code_to_delete = item_values[0]
# Убрал messagebox.askyesno для полной автоматизации (если это тест)
# Если нужно оставить диалог, верни код с messagebox обратно.
db_path = os.path.join("project_dbs", f"{self.project_code}.db")
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute(
f"""
DELETE FROM {TABLE_PROJECT_DATA}
WHERE event_code = ? AND project_code = ? AND indicator_code = ?
""",
(event_code_to_delete, self.project_code, self.indicator_code),
)
conn.commit()
if conn.total_changes > 0:
for item in selected_items:
self.events_tree.delete(item)
self.recalculate_event_codes()
except Exception as e:
pass # Или логирование ошибки
finally:
if "conn" in locals():
conn.close()
def print_to_word(self):
"""Создаёт и сохраняет файлы Excel и Word с отчётом."""
# --- 1. ПОДГОТОВИТЕЛЬНЫЙ ЭТАП ---
import os
from datetime import datetime
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root_dir = os.path.abspath(os.path.join(current_dir, ".."))
projects_dir = os.path.join(project_root_dir, "projects")
os.makedirs(projects_dir, exist_ok=True)
now = datetime.now()
timestamp = now.strftime("%Y%m%d_%H%M")
file_base_name = f"{self.project_code}_{timestamp}"
docx_file_name = f"{file_base_name}.docx"
docx_file_path = os.path.join(projects_dir, docx_file_name)
xlsx_file_name = f"{file_base_name}.xlsx"
xlsx_file_path = os.path.join(projects_dir, xlsx_file_name)
# --- 2. ПОЛУЧЕНИЕ ДАННЫХ ИЗ БАЗЫ (ГЛАВНОЕ ИЗМЕНЕНИЕ) ---
# Получаем данные для шапки (Название, Описание и т.д.)
project_name = "Неизвестный проект"
project_description = ""
date_opened = "[Дата открытия]"
responsible = "[ФИО]"
try:
import sqlite3
from tkinter import messagebox
import pandas as pd
db_path = os.path.join("project_dbs", f"{self.project_code}.db")
if not os.path.exists(db_path):
messagebox.showerror("Ошибка", "База данных не найдена.")
return
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Запрос для получения данных проекта для шапки
try:
cursor.execute(
f"""
SELECT name, description, date_opened, responsible
FROM {TABLE_PROJECT_DATA}
WHERE project_code = ?
LIMIT 1
""",
(self.project_code,),
)
header_row = cursor.fetchone()
# Если данные проекта найдены, используем их
if header_row:
project_name, project_description, date_opened, responsible = (
header_row
)
# Запрос для получения списка событий (как и раньше)
cursor.execute(
f"""
SELECT event_code, event_data, event_text_content
FROM {TABLE_PROJECT_DATA}
WHERE project_code = ?
ORDER BY group_code, indicator_code, event_data ASC
""",
(self.project_code,),
)
rows = cursor.fetchall()
except Exception as sql_error:
messagebox.showerror(
"Ошибка SQL", f"Ошибка при выполнении запроса к базе: {sql_error}"
)
return
finally:
conn.close()
if not rows:
messagebox.showwarning("Печать", "Нет событий для печати.")
return
valid_rows_data = []
for row in rows:
event_code, date_str, event_text = row
if event_text and date_str:
valid_rows_data.append((event_code, date_str, event_text))
if not valid_rows_data:
messagebox.showwarning(
"Печать",
"Нет валидных событий для печати (отсутствует дата или текст).",
)
return
except Exception as e:
messagebox.showerror(
"Ошибка подготовки", f"Не удалось подготовить данные: {e}"
)
return
# Флаги для финального сообщения
word_success = False
excel_success = False
# --- 3. СОЗДАНИЕ ВОРД ФАЙЛА ---
try:
from docx import Document
from docx.shared import Pt, Cm, Inches, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.section import WD_ORIENT
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_TABLE_DIRECTION
doc = Document()
# Настройка страницы (Альбомная)
section = doc.sections[0]
section.orientation = WD_ORIENT.LANDSCAPE
section.page_width = Cm(29.7)
section.page_height = Cm(21.0)
section.top_margin = Cm(2)
section.bottom_margin = Cm(2)
section.left_margin = Cm(3)
section.right_margin = Cm(1.5)
# Настройка шрифта
style = doc.styles["Normal"]
font = style.font
font.name = "Times New Roman"
font.size = Pt(12)
# Шапка документа (ИСПРАВЛЕННАЯ ШАПКА)
doc.add_paragraph("Отчёт о событиях").alignment = WD_ALIGN_PARAGRAPH.CENTER
# Используем переменные с данными из базы
doc.add_paragraph(f"Название проекта: {project_name}")
doc.add_paragraph(f"Код проекта: {self.project_code}")
# Добавляем Описание (которого раньше не было)
doc.add_paragraph(f"Описание: {project_description}")
doc.add_paragraph(f"Дата открытия: {date_opened}")
doc.add_paragraph(f"Ответственный: {responsible}")
doc.add_paragraph("")
# --- СОЗДАНИЕ И ОФОРМЛЕНИЕ ТАБЛИЦЫ ---
# 1. Создаем таблицу (4 столбца, 1 строка для заголовка)
table = doc.add_table(rows=1, cols=4, style="Table Grid")
table.alignment = WD_TABLE_ALIGNMENT.CENTER # Исправлено на константу
table.allow_autofit = False
table.columns[0].width = Cm(2.0)
table.columns[1].width = Cm(2.5)
table.columns[2].width = Cm(2.5)
table.columns[3].width = Inches(7.0)
table.columns[3].width_rule = 3
# --- ЗАПОЛНЕНИЕ ТАБЛИЦЫ ---
hdr_cells = table.rows[0].cells
hdr_cells[0].text = "№ п/п"
hdr_cells[1].text = "Код"
hdr_cells[2].text = "Дата"
hdr_cells[3].text = "Событие"
# Выравнивание и жирный шрифт для заголовка + ЗАЛИВКА СЕРЫМ ЦВЕТОМ
for cell in hdr_cells:
p = cell.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.runs[0]
run.font.bold = True
try:
# Пробуем новый метод (для новых версий)
shading_elm = cell._tc.get_or_add_tcPr().get_or_add_shd()
shading_elm.fill = "D9D9D9"
shading_elm.val = "clear"
except AttributeError:
# Если не получилось (для старых версий), используем XML
from docx.oxml.ns import nsdecls
from docx.oxml import parse_xml
cell._tc.get_or_add_tcPr().append(
parse_xml(f'<w:shd {nsdecls("w")} w:fill="D9D9D9"/>')
)
for i, (code, date, text) in enumerate(valid_rows_data, start=1):
cells = table.add_row().cells
cells[0].text = str(i)
cells[1].text = code
cells[2].text = date
cells[3].text = text
for j in range(3):
cells[j].paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
cells[3].paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.LEFT
doc.save(docx_file_path)
word_success = True
except Exception as e:
messagebox.showerror("Ошибка Word", f"Не удалось создать Word-файл: {e}")
word_success = False
# --- 4. СОЗДАНИЕ ЭКСЕЛЬ ФАЙЛА ---
try:
data_for_excel = [
{"№ п/п": i, "Код": code, "Дата": date, "Событие": text}
for i, (code, date, text) in enumerate(valid_rows_data, start=1)
]
df = pd.DataFrame(data_for_excel)
df.to_excel(xlsx_file_path, index=False)
excel_success = True
except Exception as e:
messagebox.showerror("Ошибка Excel", f"Не удалось создать Excel-файл: {e}")
else:
pass
# --- 5. ФИНАЛЬНОЕ СООБЩЕНИЕ ПОЛЬЗОВАТЕЛЮ ---
if word_success or excel_success:
message_text = "Файлы успешно сохранены в папку 'projects':\n"
if word_success:
message_text += f"- {docx_file_name}\n"
if excel_success:
message_text += f"- {xlsx_file_name}\n"
messagebox.showinfo("Успех", message_text.strip())
class EventEditWindow:
"""
Окно для редактирования выбранного события.
"""
def __init__(self, parent_window, event_code, date_val, direction_val, event_text):
# Сохраняем ссылку на родительское окно и данные
self.parent_window = parent_window
self.event_code = event_code
self.date_val = date_val
self.direction_val = direction_val
# Создаем само окно
self.window = tk.Toplevel(parent_window.window)
self.window.title("Редактирование событий")
self.window.geometry("450x220")
self.window.transient(parent_window.window)
self.window.grab_set()
# --- 1. СОЗДАНИЕ ФРЕЙМА И НАСТРОЙКА СЕТКИ ---
# Используем один фрейм с сеткой для всех элементов.
# Это самый надежный способ избежать проблем с .pack() и .grid().
frame = tk.Frame(self.window, padx=10, pady=10)
frame.pack(fill="both", expand=True)
# Настраиваем колонки: вторая колонка (индекс 1) будет растягиваться.
frame.columnconfigure(1, weight=1)
# --- 2. РАЗМЕЩЕНИЕ ВИДЖЕТОВ В СЕТКЕ ---
# Поле "Дата" (Строка 0)
tk.Label(frame, text="Дата:").grid(row=0, column=0, sticky="e", pady=3)
self.date_entry = tk.Entry(frame, width=25)
self.date_entry.insert(0, date_val)
# sticky="we" заставляет поле ввода растягиваться по горизонтали.
self.date_entry.grid(row=0, column=1, sticky="we", pady=3)
# Поле "Направленность" (Строка 1)
tk.Label(frame, text="Направленность:").grid(row=1, column=0, sticky="e")
# Для радиокнопок создаем отдельный фрейм, чтобы они стояли в одну линию.
radio_frame = tk.Frame(frame)
radio_frame.grid(row=1, column=1, sticky="w", pady=3)
self.dir_var = tk.IntVar(value=direction_val)
Radiobutton(radio_frame, text="+ К цели", variable=self.dir_var, value=1).pack(
side="left"
)
Radiobutton(
radio_frame, text="- От цели", variable=self.dir_var, value=-1
).pack(side="left", padx=(10, 0))
# Поле "Событие" (Строка 2)
tk.Label(frame, text="Событие:").grid(
row=2, column=0, sticky="ne", pady=(10, 3)
)
self.event_text_field = tk.Text(frame, height=4, width=35, wrap="word")
self.event_text_field.insert("1.0", event_text)
# Используем sticky="we" здесь тоже для единообразия.
self.event_text_field.grid(
row=2, column=1, sticky="we", pady=(10, 3), padx=(0, 10)
)
# Кнопка "Сохранить" (Строка 3)
save_btn = tk.Button(
frame,
text="Сохранить изменения",
command=self.save_and_close,
bg="#4CAF50", # Зеленый цвет для красоты
fg="white",
)
# Размещаем кнопку на 3-й строке, растягиваем на обе колонки (columnspan=2)
save_btn.grid(row=3, columnspan=2, pady=(20, 5), ipadx=20)
def save_and_close(self):
"""
Сохраняет изменения из полей ввода,
передает их в родительское окно и закрывает это окно.
"""
# 1. Получаем новые значения из интерфейса
new_date = self.date_entry.get()
new_direction = self.dir_var.get()
# Получаем текст из поля. "end-1c" - правильный способ избежать лишней пустой строки.
new_event_text = self.event_text_field.get("1.0", tk.END).strip()
# 2. Проверка: не пустое ли поле "Событие"?
if not new_event_text:
messagebox.showwarning("Ошибка", 'Поле "Событие" не может быть пустым.')
return # Выходим из функции, ничего не сохраняя
# 3. Передаем данные в родительское окно для обновления БД и таблицы
# self.parent_window - это ссылка на ProjectEventWindow, которую мы передали в __init__
self.parent_window.update_event_in_db(
self.event_code, # Код события, которое мы редактируем
new_date, # Новая дата
new_direction, # Новое направление (+1 или -1)
new_event_text, # Новый текст
)
# 4. Закрываем окно редактирования
self.window.destroy()
def on_close(self):
"""Закрытие окна ввода событий.
Теперь просто уничтожаем окно, не пытаясь ничего вернуть,
так как за нас это сделал open_event_window_from_list."""
try:
# Просто закрываем текущее окно.
# Логика возврата перенесена в main.py и выполняется там
# через уничтожение и создание новых окон.
self.window.destroy()
except Exception as e:
print(f"[ДИАГНОСТИКА] Ошибка при закрытии окна: {e}")