PM4.py 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069
  1. import os
  2. import csv
  3. import tkinter as tk
  4. from tkinter import ttk, messagebox
  5. import psycopg2
  6. DB_CONFIG = {
  7. "dbname": "dbwork",
  8. "user": "tokarev_pi",
  9. "password": "#8&3u&52#&D",
  10. "host": "edu-pg.itiscaf.ru",
  11. "port": "5432"
  12. }
  13. class BookWarehouseApp(tk.Tk):
  14. def __init__(self):
  15. super().__init__()
  16. self.title("Информационная система «Склад книг»")
  17. self.geometry("1150x700")
  18. # Настройка визуального стиля (UI/UX)
  19. self.setup_styles()
  20. self.current_user = None
  21. self.current_role = None
  22. self.show_login_window()
  23. def setup_styles(self):
  24. """Настраивает цветовую схему, шрифты и стили компонентов интерфейса."""
  25. self.style = ttk.Style()
  26. self.style.theme_use('clam')
  27. bg_main = "#F4F6F9"
  28. fg_main = "#333333"
  29. primary_color = "#1A73E8"
  30. btn_active = "#1557B0"
  31. self.style.configure('.', background=bg_main, foreground=fg_main, font=('Segoe UI', 10))
  32. self.style.configure('TNotebook', background=bg_main, borderwidth=0)
  33. self.style.configure('TNotebook.Tab', font=('Segoe UI', 10, 'bold'), padding=[15, 6], background='#E0E4EC')
  34. self.style.map('TNotebook.Tab', background=[('selected', primary_color)], foreground=[('selected', 'white')])
  35. self.style.configure('TButton', font=('Segoe UI', 10, 'bold'), background=primary_color, foreground='white', borderwidth=0, padding=6)
  36. self.style.map('TButton', background=[('active', btn_active), ('disabled', '#D2D6DE')], foreground=[('disabled', '#959DA5')])
  37. self.style.configure('Treeview', font=('Segoe UI', 10), rowheight=28, background='white', fieldbackground='white', borderwidth=1)
  38. self.style.configure('Treeview.Heading', font=('Segoe UI', 10, 'bold'), background='#E6E9ED', foreground=fg_main, padding=5)
  39. self.style.map('Treeview', background=[('selected', primary_color)], foreground=[('selected', 'white')])
  40. self.style.configure('TLabel', background=bg_main, font=('Segoe UI', 10))
  41. self.style.configure('TLabelframe', background=bg_main, borderwidth=1, relief="solid")
  42. self.style.configure('TLabelframe.Label', font=('Segoe UI', 10, 'bold'), foreground=primary_color, background=bg_main)
  43. def connect_db(self):
  44. """Устанавливает соединение с БД PostgreSQL."""
  45. conn = psycopg2.connect(**DB_CONFIG)
  46. conn.set_client_encoding('UTF8')
  47. return conn
  48. def safe_decode(self, value):
  49. """Безопасно преобразует любые типы данных из БД в строку."""
  50. if value is None:
  51. return ""
  52. if isinstance(value, bytes):
  53. return value.decode('utf-8', errors='ignore')
  54. return str(value)
  55. def show_login_window(self):
  56. """Отрисовывает центрированное окно авторизации и регистрации."""
  57. self.configure(background="#F4F6F9")
  58. self.login_frame = ttk.Labelframe(self, text=" Авторизация в системе ", padding=25)
  59. self.login_frame.place(relx=0.5, rely=0.5, anchor="center")
  60. ttk.Label(self.login_frame, text="Добро пожаловать!", font=("Segoe UI", 16, "bold"), foreground="#1A73E8").grid(row=0, column=0, columnspan=2, pady=15)
  61. ttk.Label(self.login_frame, text="Логин:").grid(row=1, column=0, sticky="e", padx=5, pady=8)
  62. self.ent_login = ttk.Entry(self.login_frame, font=('Segoe UI', 11), width=25)
  63. self.ent_login.grid(row=1, column=1, padx=5, pady=8)
  64. ttk.Label(self.login_frame, text="Пароль:").grid(row=2, column=0, sticky="e", padx=5, pady=8)
  65. self.ent_password = ttk.Entry(self.login_frame, show="*", font=('Segoe UI', 11), width=25)
  66. self.ent_password.grid(row=2, column=1, padx=5, pady=8)
  67. btn_frame = ttk.Frame(self.login_frame)
  68. btn_frame.grid(row=3, column=0, columnspan=2, pady=20)
  69. ttk.Button(btn_frame, text="Войти", command=self.handle_login).pack(side="left", padx=5, ipadx=10)
  70. ttk.Button(btn_frame, text="Регистрация", command=self.show_register_dialog).pack(side="left", padx=5, ipadx=10)
  71. def show_register_dialog(self):
  72. """Окно создания нового аккаунта (Регистрация)."""
  73. dialog = tk.Toplevel(self)
  74. dialog.title("Регистрация нового пользователя")
  75. dialog.geometry("400x360")
  76. dialog.grab_set()
  77. dialog.configure(background="#F4F6F9")
  78. ttk.Label(dialog, text="Регистрация аккаунта", font=("Segoe UI", 14, "bold"), foreground="#1A73E8").pack(pady=15)
  79. ttk.Label(dialog, text="Придумайте логин:").pack(pady=2, anchor="w", padx=40)
  80. ent_reg_login = ttk.Entry(dialog, font=('Segoe UI', 11), width=30)
  81. ent_reg_login.pack(padx=40, pady=4, fill="x")
  82. ttk.Label(dialog, text="Придумайте пароль:").pack(pady=2, anchor="w", padx=40)
  83. ent_reg_password = ttk.Entry(dialog, show="*", font=('Segoe UI', 11), width=30)
  84. ent_reg_password.pack(padx=40, pady=4, fill="x")
  85. ttk.Label(dialog, text="Повторите пароль:").pack(pady=2, anchor="w", padx=40)
  86. ent_reg_confirm = ttk.Entry(dialog, show="*", font=('Segoe UI', 11), width=30)
  87. ent_reg_confirm.pack(padx=40, pady=4, fill="x")
  88. def execute_registration():
  89. login = ent_reg_login.get().strip()
  90. password = ent_reg_password.get().strip()
  91. confirm = ent_reg_confirm.get().strip()
  92. if not login or not password or not confirm:
  93. messagebox.showerror("Ошибка", "Заполните все доступные поля!", parent=dialog)
  94. return
  95. if password != confirm:
  96. messagebox.showerror("Ошибка", "Введенные пароли не совпадают!", parent=dialog)
  97. return
  98. try:
  99. conn = self.connect_db()
  100. with conn:
  101. with conn.cursor() as cur:
  102. # Точное соответствие колонкам: login, password_hash, role_name
  103. cur.execute("""
  104. INSERT INTO Users (login, password_hash, role_name, employee_id)
  105. VALUES (%s, %s, 'читатель', NULL);
  106. """, (login, password))
  107. conn.close()
  108. dialog.destroy()
  109. messagebox.showinfo("Успех", "Регистрация завершена! Роль по умолчанию: читатель. Теперь вы можете войти.")
  110. except psycopg2.errors.UniqueViolation:
  111. messagebox.showerror("Ошибка", "Пользователь с таким логином уже существует!", parent=dialog)
  112. except Exception as e:
  113. messagebox.showerror("Ошибка СУБД", f"Не удалось выполнить регистрацию:\n{e}", parent=dialog)
  114. ttk.Button(dialog, text="✨ Зарегистрироваться", command=execute_registration).pack(pady=22, ipadx=10)
  115. def handle_login(self):
  116. """Проверяет данные через хранимую функцию getrole."""
  117. login = self.ent_login.get().strip()
  118. password = self.ent_password.get().strip()
  119. if not login or not password:
  120. messagebox.showerror("Ошибка", "Заполните все поля!")
  121. return
  122. try:
  123. conn = self.connect_db()
  124. with conn:
  125. with conn.cursor() as cur:
  126. cur.execute('SELECT getRole(%s::varchar, %s::varchar);', (login, password))
  127. row = cur.fetchone()
  128. conn.close()
  129. if row and row[0] is not None:
  130. role_raw = self.safe_decode(row[0]).strip().upper()
  131. self.current_user = login
  132. # Если роль содержит ADMIN или АДМИН — даем полные права, иначе — обычные
  133. self.current_role = "ADMIN" if any(x in role_raw for x in ["АДМИН", "ADMIN"]) else "EMPLOYEE"
  134. self.login_frame.destroy()
  135. self.show_main_window()
  136. else:
  137. messagebox.showerror("Ошибка", "Неверный логин или пароль")
  138. except Exception as e:
  139. messagebox.showerror("Ошибка подключения", f"Ошибка базы данных:\n{e}")
  140. def show_main_window(self):
  141. """Инициализирует главное рабочее пространство системы с вкладками."""
  142. status_frame = ttk.Frame(self, padding=10, relief="groove")
  143. status_frame.pack(side="top", fill="x")
  144. ttk.Label(status_frame, text=f"👤 Пользователь: {self.current_user} | 💼 Уровень прав: {self.current_role}", font=("Segoe UI", 10, "bold"), foreground="#555555").pack(side="left")
  145. ttk.Button(status_frame, text="🚪 Выйти из аккаунта", command=self.logout).pack(side="right")
  146. self.notebook = ttk.Notebook(self, padding=10)
  147. self.notebook.pack(expand=True, fill="both")
  148. self.create_books_tab()
  149. self.create_warehouse_tab()
  150. self.create_reservations_tab()
  151. self.create_personnel_tab()
  152. def logout(self):
  153. """Уничтожает текущий интерфейс и возвращает пользователя на форму логина."""
  154. for widget in self.winfo_children():
  155. widget.destroy()
  156. self.show_login_window()
  157. # --- ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ СВЯЗЕЙ СУБД ---
  158. def get_authors_list(self):
  159. return self._fetch_lookup('SELECT author_id, author_name FROM Authors ORDER BY author_name;')
  160. def get_categories_list(self):
  161. return self._fetch_lookup('SELECT category_id, category_name FROM Categories ORDER BY category_name;')
  162. def get_books_list(self):
  163. return self._fetch_lookup('SELECT book_id, title FROM Books ORDER BY title;')
  164. def get_personnel_list(self):
  165. return self._fetch_lookup("SELECT employee_id, surname || ' ' || name FROM Personnel ORDER BY surname;")
  166. def get_positions_list(self):
  167. """Загружает список доступных должностей для Personnel constraint."""
  168. return self._fetch_lookup("SELECT position_id, position_name FROM Positions ORDER BY position_name;")
  169. def _fetch_lookup(self, sql):
  170. res = []
  171. try:
  172. conn = self.connect_db()
  173. with conn:
  174. with conn.cursor() as cur:
  175. cur.execute(sql)
  176. res = [(row[0], self.safe_decode(row[1])) for row in cur.fetchall()]
  177. conn.close()
  178. except Exception as e:
  179. print(f"Ошибка lookup: {e}")
  180. return res
  181. # ==========================================
  182. # ВКЛАДКА 1: КНИГИ (ПРОСМОТР + CRUD)
  183. # ==========================================
  184. def create_books_tab(self):
  185. tab = ttk.Frame(self.notebook, padding=10)
  186. self.notebook.add(tab, text="📚 Книги")
  187. search_frame = ttk.LabelFrame(tab, text=" Поиск книги по названию ", padding=10)
  188. search_frame.pack(fill="x", padx=5, pady=5)
  189. ttk.Label(search_frame, text="Название:").pack(side="left", padx=5)
  190. self.book_search_var = tk.StringVar()
  191. self.book_search_var.trace_add("write", lambda *args: self.load_books_data())
  192. ttk.Entry(search_frame, textvariable=self.book_search_var, font=('Segoe UI', 10)).pack(side="left", fill="x", expand=True, padx=5, pady=5)
  193. ttk.Button(search_frame, text="📥 Экспорт в CSV", command=lambda: self.export_to_csv(self.books_tree, "books_export.csv")).pack(side="right", padx=5)
  194. columns = ("id", "title", "author", "category", "price")
  195. self.books_tree = ttk.Treeview(tab, columns=columns, show="headings")
  196. self.books_tree.heading("id", text="ID")
  197. self.books_tree.heading("title", text="Название книги")
  198. self.books_tree.heading("author", text="Автор")
  199. self.books_tree.heading("category", text="Категория")
  200. self.books_tree.heading("price", text="Цена (руб.)")
  201. self.books_tree.column("id", width=60, anchor="center", stretch=tk.NO)
  202. self.books_tree.column("title", width=300, anchor="w", stretch=tk.YES)
  203. self.books_tree.column("author", width=200, anchor="w", stretch=tk.YES)
  204. self.books_tree.column("category", width=150, anchor="w", stretch=tk.YES)
  205. self.books_tree.column("price", width=100, anchor="e", stretch=tk.NO)
  206. self.books_tree.pack(expand=True, fill="both", padx=5, pady=5)
  207. btn_frame = ttk.Frame(tab, padding=5)
  208. btn_frame.pack(fill="x", padx=5, pady=5)
  209. ttk.Button(btn_frame, text="➕ Добавить книгу", command=self.add_book_dialog).pack(side="left", padx=5)
  210. ttk.Button(btn_frame, text="✏️ Редактировать", command=self.edit_book_dialog).pack(side="left", padx=5)
  211. ttk.Button(btn_frame, text="❌ Удалить книгу", command=self.delete_book).pack(side="left", padx=5)
  212. self.load_books_data()
  213. def load_books_data(self):
  214. for i in self.books_tree.get_children():
  215. self.books_tree.delete(i)
  216. search_query = self.book_search_var.get()
  217. try:
  218. conn = self.connect_db()
  219. with conn:
  220. with conn.cursor() as cur:
  221. query = """
  222. SELECT b.book_id, b.title, a.author_name, c.category_name, b.price
  223. FROM Books b
  224. JOIN Authors a ON b.author_id = a.author_id
  225. JOIN Categories c ON b.category_id = c.category_id
  226. WHERE b.title ILIKE %s ORDER BY b.book_id DESC;
  227. """
  228. cur.execute(query, (f"%{search_query}%",))
  229. for row in cur.fetchall():
  230. self.books_tree.insert("", "end", values=[self.safe_decode(cell) for cell in row])
  231. conn.close()
  232. except Exception as e:
  233. print(f"Ошибка загрузки книг: {e}")
  234. def add_book_dialog(self):
  235. authors_list = self.get_authors_list()
  236. categories_list = self.get_categories_list()
  237. if not authors_list or not categories_list:
  238. messagebox.showerror("Ошибка", "Списки авторов или категорий пусты в БД!")
  239. return
  240. dialog = tk.Toplevel(self)
  241. dialog.title("Добавление книги")
  242. dialog.geometry("400x320")
  243. dialog.grab_set()
  244. ttk.Label(dialog, text="Название книги:").pack(pady=2, anchor="w", padx=20)
  245. ent_title = ttk.Entry(dialog, width=40)
  246. ent_title.pack(padx=20, pady=2, fill="x")
  247. ttk.Label(dialog, text="Автор:").pack(pady=2, anchor="w", padx=20)
  248. cb_author = ttk.Combobox(dialog, values=[a[1] for a in authors_list], state="readonly")
  249. cb_author.pack(padx=20, pady=2, fill="x")
  250. cb_author.current(0)
  251. ttk.Label(dialog, text="Категория:").pack(pady=2, anchor="w", padx=20)
  252. cb_category = ttk.Combobox(dialog, values=[c[1] for c in categories_list], state="readonly")
  253. cb_category.pack(padx=20, pady=2, fill="x")
  254. cb_category.current(0)
  255. ttk.Label(dialog, text="Цена:").pack(pady=2, anchor="w", padx=20)
  256. ent_price = ttk.Entry(dialog, width=40)
  257. ent_price.pack(padx=20, pady=2, fill="x")
  258. def save():
  259. title = ent_title.get().strip()
  260. price_str = ent_price.get().strip()
  261. if not title or not price_str:
  262. messagebox.showerror("Ошибка", "Заполните все поля!", parent=dialog)
  263. return
  264. try:
  265. price = float(price_str)
  266. except ValueError:
  267. messagebox.showerror("Ошибка", "Цена должна быть числом!", parent=dialog)
  268. return
  269. author_id = authors_list[cb_author.current()][0]
  270. category_id = categories_list[cb_category.current()][0]
  271. try:
  272. conn = self.connect_db()
  273. with conn:
  274. with conn.cursor() as cur:
  275. cur.execute('INSERT INTO Books (title, price, author_id, category_id) VALUES (%s, %s, %s, %s)',
  276. (title, price, author_id, category_id))
  277. conn.close()
  278. self.load_books_data()
  279. dialog.destroy()
  280. messagebox.showinfo("Успех", "Книга успешно добавлена!")
  281. except Exception as e:
  282. messagebox.showerror("Ошибка", f"Не удалось добавить книгу: {e}", parent=dialog)
  283. ttk.Button(dialog, text="💾 Сохранить", command=save).pack(pady=20)
  284. def edit_book_dialog(self):
  285. selected = self.books_tree.selection()
  286. if not selected:
  287. messagebox.showwarning("Внимание", "Выберите книгу для редактирования!")
  288. return
  289. book_values = self.books_tree.item(selected[0])['values']
  290. book_id, current_title, current_author_name, current_category_name, current_price = book_values
  291. authors_list = self.get_authors_list()
  292. categories_list = self.get_categories_list()
  293. dialog = tk.Toplevel(self)
  294. dialog.title(f"Редактирование книги (ID: {book_id})")
  295. dialog.geometry("400x320")
  296. dialog.grab_set()
  297. ttk.Label(dialog, text="Название книги:").pack(pady=2, anchor="w", padx=20)
  298. ent_title = ttk.Entry(dialog, width=40)
  299. ent_title.pack(padx=20, pady=2, fill="x")
  300. ent_title.insert(0, current_title)
  301. ttk.Label(dialog, text="Автор:").pack(pady=2, anchor="w", padx=20)
  302. author_names = [a[1] for a in authors_list]
  303. cb_author = ttk.Combobox(dialog, values=author_names, state="readonly")
  304. cb_author.pack(padx=20, pady=2, fill="x")
  305. if current_author_name in author_names:
  306. cb_author.current(author_names.index(current_author_name))
  307. ttk.Label(dialog, text="Категория:").pack(pady=2, anchor="w", padx=20)
  308. category_names = [c[1] for c in categories_list]
  309. cb_category = ttk.Combobox(dialog, values=category_names, state="readonly")
  310. cb_category.pack(padx=20, pady=2, fill="x")
  311. if current_category_name in category_names:
  312. cb_category.current(category_names.index(current_category_name))
  313. ttk.Label(dialog, text="Цена:").pack(pady=2, anchor="w", padx=20)
  314. ent_price = ttk.Entry(dialog, width=40)
  315. ent_price.pack(padx=20, pady=2, fill="x")
  316. ent_price.insert(0, current_price)
  317. def update_save():
  318. title = ent_title.get().strip()
  319. price_str = ent_price.get().strip()
  320. if not title or not price_str:
  321. messagebox.showerror("Ошибка", "Заполните все поля!", parent=dialog)
  322. return
  323. try:
  324. price = float(price_str)
  325. except ValueError:
  326. messagebox.showerror("Ошибка", "Цена должна быть числом!", parent=dialog)
  327. return
  328. author_id = authors_list[cb_author.current()][0]
  329. category_id = categories_list[cb_category.current()][0]
  330. try:
  331. conn = self.connect_db()
  332. with conn:
  333. with conn.cursor() as cur:
  334. cur.execute('''
  335. UPDATE Books
  336. SET title = %s, price = %s, author_id = %s, category_id = %s
  337. WHERE book_id = %s;
  338. ''', (title, price, author_id, category_id, book_id))
  339. conn.close()
  340. self.load_books_data()
  341. dialog.destroy()
  342. messagebox.showinfo("Успех", "Данные книги успешно обновлены!")
  343. except Exception as e:
  344. messagebox.showerror("Ошибка", f"Не удалось обновить данные: {e}", parent=dialog)
  345. ttk.Button(dialog, text="🔄 Обновить", command=update_save).pack(pady=20)
  346. def delete_book(self):
  347. selected = self.books_tree.selection()
  348. if not selected:
  349. messagebox.showwarning("Внимание", "Выберите книгу для удаления!")
  350. return
  351. book_id = self.books_tree.item(selected[0])['values'][0]
  352. book_title = self.books_tree.item(selected[0])['values'][1]
  353. if messagebox.askyesno("Подтверждение", f"Вы уверены, что хотите удалить '{book_title}'?\nЭто каскадно очистит связанные остатки склада!"):
  354. try:
  355. conn = self.connect_db()
  356. with conn:
  357. with conn.cursor() as cur:
  358. cur.execute('DELETE FROM Warehouse WHERE book_id = %s;', (book_id,))
  359. cur.execute('DELETE FROM Reservation_Books WHERE book_id = %s;', (book_id,))
  360. cur.execute('DELETE FROM Books WHERE book_id = %s;', (book_id,))
  361. conn.close()
  362. self.load_books_data()
  363. self.load_warehouse_data()
  364. messagebox.showinfo("Успех", f"Книга '{book_title}' успешно удалена!")
  365. except Exception as e:
  366. messagebox.showerror("Ошибка удаления", f"Критический сбой СУБД:\n{e}")
  367. # ==========================================
  368. # ВКЛАДКА 2: СКЛАД (ПРОСМОТР + CRUD)
  369. # ==========================================
  370. def create_warehouse_tab(self):
  371. tab = ttk.Frame(self.notebook, padding=10)
  372. self.notebook.add(tab, text="🏢 Склад")
  373. action_frame = ttk.Frame(tab, padding=5)
  374. action_frame.pack(fill="x", side="top", pady=5)
  375. ttk.Button(action_frame, text="📥 Экспорт остатков (CSV)", command=lambda: self.export_to_csv(self.wh_tree, "warehouse_export.csv")).pack(side="right", padx=5)
  376. columns = ("book_id", "title", "author", "quantity", "price", "total_cost")
  377. self.wh_tree = ttk.Treeview(tab, columns=columns, show="headings")
  378. self.wh_tree.heading("book_id", text="ID Книги")
  379. self.wh_tree.heading("title", text="Название книги")
  380. self.wh_tree.heading("author", text="Автор")
  381. self.wh_tree.heading("quantity", text="Количество на складе")
  382. self.wh_tree.heading("price", text="Цена за ед.")
  383. self.wh_tree.heading("total_cost", text="Общая стоимость")
  384. self.wh_tree.column("book_id", width=80, anchor="center")
  385. self.wh_tree.column("title", width=250, anchor="w")
  386. self.wh_tree.column("author", width=180, anchor="w")
  387. self.wh_tree.column("quantity", width=130, anchor="center")
  388. self.wh_tree.column("price", width=100, anchor="e")
  389. self.wh_tree.column("total_cost", width=120, anchor="e")
  390. self.wh_tree.pack(expand=True, fill="both", padx=5, pady=5)
  391. btn_frame = ttk.Frame(tab, padding=5)
  392. btn_frame.pack(fill="x", padx=5, pady=5)
  393. ttk.Button(btn_frame, text="➕ Принять на склад", command=self.add_warehouse_dialog).pack(side="left", padx=5)
  394. ttk.Button(btn_frame, text="✏️ Изменить количество", command=self.edit_warehouse_dialog).pack(side="left", padx=5)
  395. ttk.Button(btn_frame, text="❌ Списать со склада", command=self.delete_warehouse).pack(side="left", padx=5)
  396. self.load_warehouse_data()
  397. def load_warehouse_data(self):
  398. """Загрузка данных из системного представления BookStockInfo."""
  399. for i in self.wh_tree.get_children():
  400. self.wh_tree.delete(i)
  401. try:
  402. conn = self.connect_db()
  403. with conn:
  404. with conn.cursor() as cur:
  405. cur.execute('SELECT * FROM BookStockInfo;')
  406. for row in cur.fetchall():
  407. self.wh_tree.insert("", "end", values=[self.safe_decode(cell) for cell in row])
  408. conn.close()
  409. except Exception as e:
  410. print(f"Ошибка загрузки представления склада: {e}")
  411. def add_warehouse_dialog(self):
  412. books_list = self.get_books_list()
  413. if not books_list:
  414. messagebox.showerror("Ошибка", "Список книг пуст!")
  415. return
  416. dialog = tk.Toplevel(self)
  417. dialog.title("Приемка на склад")
  418. dialog.geometry("400x220")
  419. dialog.grab_set()
  420. ttk.Label(dialog, text="Выберите книгу:").pack(pady=5, anchor="w", padx=20)
  421. cb_book = ttk.Combobox(dialog, values=[b[1] for b in books_list], state="readonly")
  422. cb_book.pack(padx=20, pady=2, fill="x")
  423. cb_book.current(0)
  424. ttk.Label(dialog, text="Количество:").pack(pady=5, anchor="w", padx=20)
  425. ent_qty = ttk.Entry(dialog, width=40)
  426. ent_qty.pack(padx=20, pady=2, fill="x")
  427. def save():
  428. qty_str = ent_qty.get().strip()
  429. if not qty_str:
  430. messagebox.showerror("Ошибка", "Укажите количество!", parent=dialog)
  431. return
  432. try:
  433. qty = int(qty_str)
  434. if qty < 0: raise ValueError
  435. except ValueError:
  436. messagebox.showerror("Ошибка", "Должно быть целым положительным числом!", parent=dialog)
  437. return
  438. book_id = books_list[cb_book.current()][0]
  439. try:
  440. conn = self.connect_db()
  441. with conn:
  442. with conn.cursor() as cur:
  443. cur.execute('SELECT quantity FROM Warehouse WHERE book_id = %s;', (book_id,))
  444. row = cur.fetchone()
  445. if row:
  446. cur.execute('UPDATE Warehouse SET quantity = quantity + %s WHERE book_id = %s;', (qty, book_id))
  447. else:
  448. cur.execute('INSERT INTO Warehouse (book_id, quantity) VALUES (%s, %s);', (book_id, qty))
  449. conn.close()
  450. self.load_warehouse_data()
  451. dialog.destroy()
  452. messagebox.showinfo("Успех", "Товар оприходован!")
  453. except Exception as e:
  454. messagebox.showerror("Ошибка", f"Не удалось выполнить операцию: {e}", parent=dialog)
  455. ttk.Button(dialog, text="💾 Оприходовать", command=save).pack(pady=20)
  456. def edit_warehouse_dialog(self):
  457. selected = self.wh_tree.selection()
  458. if not selected:
  459. messagebox.showwarning("Внимание", "Выберите строку для корректировки остатка!")
  460. return
  461. values = self.wh_tree.item(selected[0])['values']
  462. book_id, book_title, _, current_qty, _, _ = values
  463. dialog = tk.Toplevel(self)
  464. dialog.title("Корректировка остатков")
  465. dialog.geometry("400x180")
  466. dialog.grab_set()
  467. ttk.Label(dialog, text=f"Книга: {book_title}", font=("Segoe UI", 10, "bold"), wraplength=350).pack(pady=10, anchor="w", padx=20)
  468. ttk.Label(dialog, text="Новое количество на складе:").pack(pady=2, anchor="w", padx=20)
  469. ent_qty = ttk.Entry(dialog, width=40)
  470. ent_qty.pack(padx=20, pady=2, fill="x")
  471. ent_qty.insert(0, str(current_qty))
  472. def update():
  473. qty_str = ent_qty.get().strip()
  474. if not qty_str:
  475. messagebox.showerror("Ошибка", "Заполните поле!", parent=dialog)
  476. return
  477. try:
  478. qty = int(qty_str)
  479. if qty < 0: raise ValueError
  480. except ValueError:
  481. messagebox.showerror("Ошибка", "Введите целое положительное число!", parent=dialog)
  482. return
  483. try:
  484. conn = self.connect_db()
  485. with conn:
  486. with conn.cursor() as cur:
  487. cur.execute('UPDATE Warehouse SET quantity = %s WHERE book_id = %s;', (qty, book_id))
  488. conn.close()
  489. self.load_warehouse_data()
  490. dialog.destroy()
  491. messagebox.showinfo("Успех", "Количество обновлено!")
  492. except Exception as e:
  493. messagebox.showerror("Ошибка", f"Не удалось обновить: {e}", parent=dialog)
  494. ttk.Button(dialog, text="🔄 Обновить остаток", command=update).pack(pady=15)
  495. def delete_warehouse(self):
  496. selected = self.wh_tree.selection()
  497. if not selected:
  498. messagebox.showwarning("Внимание", "Выберите позицию для списания!")
  499. return
  500. values = self.wh_tree.item(selected[0])['values']
  501. book_id, book_title = values[0], values[1]
  502. if messagebox.askyesno("Подтверждение", f"Удалить '{book_title}' со склада? Строка остатков будет стерта."):
  503. try:
  504. conn = self.connect_db()
  505. with conn:
  506. with conn.cursor() as cur:
  507. cur.execute('DELETE FROM Warehouse WHERE book_id = %s;', (book_id,))
  508. conn.close()
  509. self.load_warehouse_data()
  510. messagebox.showinfo("Успех", "Позиция списана.")
  511. except Exception as e:
  512. messagebox.showerror("Ошибка списания", f"Не удалось удалить: {e}")
  513. # ==========================================
  514. # ВКЛАДКА 3: БРОНИРОВАНИЕ (ЖУРНАЛ + CRUD)
  515. # ==========================================
  516. def create_reservations_tab(self):
  517. tab = ttk.Frame(self.notebook, padding=10)
  518. self.notebook.add(tab, text="📋 Бронирование")
  519. columns = ("res_id", "date", "employee")
  520. self.res_tree = ttk.Treeview(tab, columns=columns, show="headings")
  521. self.res_tree.heading("res_id", text="ID Брони")
  522. self.res_tree.heading("date", text="Дата оформления")
  523. self.res_tree.heading("employee", text="Ответственный сотрудник")
  524. self.res_tree.column("res_id", width=100, anchor="center")
  525. self.res_tree.column("date", width=250, anchor="center")
  526. self.res_tree.column("employee", width=400, anchor="w")
  527. self.res_tree.pack(expand=True, fill="both", padx=5, pady=5)
  528. btn_frame = ttk.Frame(tab, padding=5)
  529. btn_frame.pack(fill="x", padx=5, pady=5)
  530. ttk.Button(btn_frame, text="➕ Создать новую бронь", command=self.add_reservation_dialog).pack(side="left", padx=5)
  531. ttk.Button(btn_frame, text="✏️ Изменить сотрудника", command=self.edit_reservation_dialog).pack(side="left", padx=5)
  532. ttk.Button(btn_frame, text="❌ Аннулировать бронь", command=self.delete_reservation).pack(side="left", padx=5)
  533. self.load_reservations_data()
  534. def load_reservations_data(self):
  535. for i in self.res_tree.get_children():
  536. self.res_tree.delete(i)
  537. try:
  538. conn = self.connect_db()
  539. with conn:
  540. with conn.cursor() as cur:
  541. query = """
  542. SELECT r.reservation_id, r.reservation_date, p.surname || ' ' || p.name || ' ' || COALESCE(p.patronymic, '')
  543. FROM Reservations r
  544. JOIN Personnel p ON r.employee_id = p.employee_id
  545. ORDER BY r.reservation_id DESC;
  546. """
  547. cur.execute(query)
  548. for row in cur.fetchall():
  549. self.res_tree.insert("", "end", values=[self.safe_decode(cell) for cell in row])
  550. conn.close()
  551. except Exception as e:
  552. print(f"Ошибка загрузки броней: {e}")
  553. def add_reservation_dialog(self):
  554. pers_list = self.get_personnel_list()
  555. if not pers_list:
  556. messagebox.showerror("Ошибка", "Штат персонала пуст!")
  557. return
  558. dialog = tk.Toplevel(self)
  559. dialog.title("Новое бронирование")
  560. dialog.geometry("400x180")
  561. dialog.grab_set()
  562. ttk.Label(dialog, text="Выберите ответственного сотрудника:").pack(pady=10, anchor="w", padx=20)
  563. cb_pers = ttk.Combobox(dialog, values=[p[1] for p in pers_list], state="readonly")
  564. cb_pers.pack(padx=20, pady=5, fill="x")
  565. cb_pers.current(0)
  566. def make():
  567. emp_id = pers_list[cb_pers.current()][0]
  568. try:
  569. conn = self.connect_db()
  570. with conn:
  571. with conn.cursor() as cur:
  572. cur.execute('INSERT INTO Reservations (employee_id) VALUES (%s) RETURNING reservation_id;', (emp_id,))
  573. new_id = cur.fetchone()[0]
  574. conn.close()
  575. self.load_reservations_data()
  576. dialog.destroy()
  577. messagebox.showinfo("Успех", f"Успешно оформлена бронь №{new_id}!")
  578. except Exception as e:
  579. messagebox.showerror("Ошибка", f"Не удалось создать бронь: {e}", parent=dialog)
  580. ttk.Button(dialog, text="💾 Зарегистрировать бронь", command=make).pack(pady=20)
  581. def edit_reservation_dialog(self):
  582. selected = self.res_tree.selection()
  583. if not selected:
  584. messagebox.showwarning("Внимание", "Выберите бронь для изменения!")
  585. return
  586. res_id, _, current_emp = self.res_tree.item(selected[0])['values']
  587. pers_list = self.get_personnel_list()
  588. dialog = tk.Toplevel(self)
  589. dialog.title(f"Редактирование брони №{res_id}")
  590. dialog.geometry("400x180")
  591. dialog.grab_set()
  592. ttk.Label(dialog, text="Изменить сотрудника:").pack(pady=10, anchor="w", padx=20)
  593. names = [p[1] for p in pers_list]
  594. cb_pers = ttk.Combobox(dialog, values=names, state="readonly")
  595. cb_pers.pack(padx=20, pady=5, fill="x")
  596. for i, n in enumerate(names):
  597. if current_emp.startswith(n) or n.startswith(current_emp):
  598. cb_pers.current(i)
  599. break
  600. if not cb_pers.get() and names:
  601. cb_pers.current(0)
  602. def update():
  603. emp_id = pers_list[cb_pers.current()][0]
  604. try:
  605. conn = self.connect_db()
  606. with conn:
  607. with conn.cursor() as cur:
  608. cur.execute('UPDATE Reservations SET employee_id = %s WHERE reservation_id = %s;', (emp_id, res_id))
  609. conn.close()
  610. self.load_reservations_data()
  611. dialog.destroy()
  612. messagebox.showinfo("Успех", "Ответственный успешно изменен!")
  613. except Exception as e:
  614. messagebox.showerror("Ошибка", f"Не удалось изменить: {e}", parent=dialog)
  615. ttk.Button(dialog, text="🔄 Обновить бронь", command=update).pack(pady=20)
  616. def delete_reservation(self):
  617. selected = self.res_tree.selection()
  618. if not selected:
  619. messagebox.showwarning("Внимание", "Выберите бронь для аннулирования!")
  620. return
  621. res_id = self.res_tree.item(selected[0])['values'][0]
  622. if messagebox.askyesno("Подтверждение", f"Вы уверены, что хотите полностью удалить бронь №{res_id}?"):
  623. try:
  624. conn = self.connect_db()
  625. with conn:
  626. with conn.cursor() as cur:
  627. cur.execute('DELETE FROM Reservation_Books WHERE reservation_id = %s;', (res_id,))
  628. cur.execute('DELETE FROM Reservations WHERE reservation_id = %s;', (res_id,))
  629. conn.close()
  630. self.load_reservations_data()
  631. messagebox.showinfo("Успех", f"Бронь №{res_id} удалена.")
  632. except Exception as e:
  633. messagebox.showerror("Ошибка удаления", f"Не удалось удалить:\n{e}")
  634. # ==========================================
  635. # ВКЛАДКА 4: УПРАВЛЕНИЕ ПЕРСОНАЛОМ (CRUD)
  636. # ==========================================
  637. def create_personnel_tab(self):
  638. tab = ttk.Frame(self.notebook, padding=10)
  639. self.notebook.add(tab, text="👥 Управление Персоналом")
  640. columns = ("id", "fio", "position", "birth", "passport", "phone", "salary")
  641. self.pers_tree = ttk.Treeview(tab, columns=columns, show="headings")
  642. self.pers_tree.heading("id", text="ID")
  643. self.pers_tree.heading("fio", text="ФИО Сотрудника")
  644. self.pers_tree.heading("position", text="Должность")
  645. self.pers_tree.heading("birth", text="Дата рожд.")
  646. self.pers_tree.heading("passport", text="Паспортные данные")
  647. self.pers_tree.heading("phone", text="Телефон")
  648. self.pers_tree.heading("salary", text="Оклад (руб.)")
  649. self.pers_tree.column("id", width=50, anchor="center", stretch=tk.NO)
  650. self.pers_tree.column("fio", width=220, stretch=tk.YES)
  651. self.pers_tree.column("position", width=140, stretch=tk.YES)
  652. self.pers_tree.column("birth", width=100, anchor="center", stretch=tk.NO)
  653. self.pers_tree.column("passport", width=140, anchor="center", stretch=tk.NO)
  654. self.pers_tree.column("phone", width=130, anchor="center", stretch=tk.NO)
  655. self.pers_tree.column("salary", width=100, anchor="e", stretch=tk.NO)
  656. self.pers_tree.pack(expand=True, fill="both", padx=5, pady=5)
  657. btn_frame = ttk.Frame(tab, padding=5)
  658. btn_frame.pack(fill="x", padx=5, pady=5)
  659. ttk.Button(btn_frame, text="➕ Нанять сотрудника", command=self.add_personnel_dialog).pack(side="left", padx=5)
  660. ttk.Button(btn_frame, text="✏️ Редактировать анкету", command=self.edit_personnel_dialog).pack(side="left", padx=5)
  661. ttk.Button(btn_frame, text="❌ Уволить сотрудника", command=self.delete_personnel).pack(side="left", padx=5)
  662. self.load_personnel_data()
  663. def load_personnel_data(self):
  664. for i in self.pers_tree.get_children():
  665. self.pers_tree.delete(i)
  666. try:
  667. conn = self.connect_db()
  668. with conn:
  669. with conn.cursor() as cur:
  670. # Добавлен JOIN таблицы Positions для корректного отображения
  671. query = """
  672. SELECT p.employee_id, p.surname || ' ' || p.name || ' ' || COALESCE(p.patronymic, ''),
  673. pos.position_name, p.birth_date, p.passport_series || ' ' || p.passport_number, p.phone, p.salary
  674. FROM Personnel p
  675. JOIN Positions pos ON p.position_id = pos.position_id
  676. ORDER BY p.employee_id DESC;
  677. """
  678. cur.execute(query)
  679. for row in cur.fetchall():
  680. self.pers_tree.insert("", "end", values=[self.safe_decode(cell) for cell in row])
  681. conn.close()
  682. except Exception as e:
  683. print(f"Ошибка загрузки персонала: {e}")
  684. def add_personnel_dialog(self):
  685. positions_list = self.get_positions_list()
  686. if not positions_list:
  687. messagebox.showerror("Ошибка", "Таблица должностей Positions пуста в БД!")
  688. return
  689. dialog = tk.Toplevel(self)
  690. dialog.title("Регистрация сотрудника")
  691. dialog.geometry("460x520")
  692. dialog.grab_set()
  693. ttk.Label(dialog, text="Фамилия:").pack(anchor="w", padx=20, pady=2)
  694. ent_surname = ttk.Entry(dialog)
  695. ent_surname.pack(fill="x", padx=20, pady=2)
  696. ttk.Label(dialog, text="Имя:").pack(anchor="w", padx=20, pady=2)
  697. ent_name = ttk.Entry(dialog)
  698. ent_name.pack(fill="x", padx=20, pady=2)
  699. ttk.Label(dialog, text="Отчество:").pack(anchor="w", padx=20, pady=2)
  700. ent_patr = ttk.Entry(dialog)
  701. ent_patr.pack(fill="x", padx=20, pady=2)
  702. ttk.Label(dialog, text="Выбрать должность:").pack(anchor="w", padx=20, pady=2)
  703. cb_pos = ttk.Combobox(dialog, values=[p[1] for p in positions_list], state="readonly")
  704. cb_pos.pack(fill="x", padx=20, pady=2)
  705. cb_pos.current(0)
  706. ttk.Label(dialog, text="Дата рождения (ГГГГ-ММ-ДД):").pack(anchor="w", padx=20, pady=2)
  707. ent_birth = ttk.Entry(dialog)
  708. ent_birth.pack(fill="x", padx=20, pady=2)
  709. ent_birth.insert(0, "1995-01-01")
  710. ttk.Label(dialog, text="Серия паспорта (4 цифры):").pack(anchor="w", padx=20, pady=2)
  711. ent_pass_ser = ttk.Entry(dialog)
  712. ent_pass_ser.pack(fill="x", padx=20, pady=2)
  713. ttk.Label(dialog, text="Номер паспорта (6 цифр):").pack(anchor="w", padx=20, pady=2)
  714. ent_pass_num = ttk.Entry(dialog)
  715. ent_pass_num.pack(fill="x", padx=20, pady=2)
  716. ttk.Label(dialog, text="Телефон:").pack(anchor="w", padx=20, pady=2)
  717. ent_phone = ttk.Entry(dialog)
  718. ent_phone.pack(fill="x", padx=20, pady=2)
  719. ttk.Label(dialog, text="Оклад:").pack(anchor="w", padx=20, pady=2)
  720. ent_salary = ttk.Entry(dialog)
  721. ent_salary.pack(fill="x", padx=20, pady=2)
  722. def save():
  723. s = ent_surname.get().strip()
  724. n = ent_name.get().strip()
  725. p = ent_patr.get().strip()
  726. b = ent_birth.get().strip()
  727. ps = ent_pass_ser.get().strip()
  728. pn = ent_pass_num.get().strip()
  729. ph = ent_phone.get().strip()
  730. sal_str = ent_salary.get().strip()
  731. if not s or not n or not b or not ps or not pn or not sal_str:
  732. messagebox.showerror("Ошибка", "Заполните обязательные поля!", parent=dialog)
  733. return
  734. try:
  735. salary = float(sal_str)
  736. except ValueError:
  737. messagebox.showerror("Ошибка", "Оклад должен быть числом!", parent=dialog)
  738. return
  739. # Получаем корректный position_id из СУБД для внешнего ключа
  740. position_id = positions_list[cb_pos.current()][0]
  741. try:
  742. conn = self.connect_db()
  743. with conn:
  744. with conn.cursor() as cur:
  745. cur.execute("""
  746. INSERT INTO Personnel (surname, name, patronymic, birth_date, passport_series, passport_number, phone, salary, position_id)
  747. VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s);
  748. """, (s, n, p if p else None, b, ps, pn, ph if ph else None, salary, position_id))
  749. conn.close()
  750. self.load_personnel_data()
  751. dialog.destroy()
  752. messagebox.showinfo("Успех", "Сотрудник принят в штат!")
  753. except Exception as e:
  754. messagebox.showerror("Ошибка СУБД", f"Не удалось добавить: {e}", parent=dialog)
  755. ttk.Button(dialog, text="💾 Сохранить анкету", command=save).pack(pady=15)
  756. def edit_personnel_dialog(self):
  757. selected = self.pers_tree.selection()
  758. if not selected:
  759. messagebox.showwarning("Внимание", "Выберите сотрудника для изменения!")
  760. return
  761. values = self.pers_tree.item(selected[0])['values']
  762. emp_id, full_fio, current_pos_name, birth, passport, phone, salary = values
  763. fio_parts = full_fio.split()
  764. s_val = fio_parts[0] if len(fio_parts) > 0 else ""
  765. n_val = fio_parts[1] if len(fio_parts) > 1 else ""
  766. p_val = fio_parts[2] if len(fio_parts) > 2 else ""
  767. pass_parts = str(passport).split()
  768. ps_val = pass_parts[0] if len(pass_parts) > 0 else ""
  769. pn_val = pass_parts[1] if len(pass_parts) > 1 else ""
  770. positions_list = self.get_positions_list()
  771. dialog = tk.Toplevel(self)
  772. dialog.title(f"Анкета сотрудника (ID: {emp_id})")
  773. dialog.geometry("460x520")
  774. dialog.grab_set()
  775. ttk.Label(dialog, text="Фамилия:").pack(anchor="w", padx=20, pady=2)
  776. ent_surname = ttk.Entry(dialog)
  777. ent_surname.pack(fill="x", padx=20, pady=2)
  778. ent_surname.insert(0, s_val)
  779. ttk.Label(dialog, text="Имя:").pack(anchor="w", padx=20, pady=2)
  780. ent_name = ttk.Entry(dialog)
  781. ent_name.pack(fill="x", padx=20, pady=2)
  782. ent_name.insert(0, n_val)
  783. ttk.Label(dialog, text="Отчество:").pack(anchor="w", padx=20, pady=2)
  784. ent_patr = ttk.Entry(dialog)
  785. ent_patr.pack(fill="x", padx=20, pady=2)
  786. ent_patr.insert(0, p_val)
  787. ttk.Label(dialog, text="Изменить должность:").pack(anchor="w", padx=20, pady=2)
  788. pos_names = [p[1] for p in positions_list]
  789. cb_pos = ttk.Combobox(dialog, values=pos_names, state="readonly")
  790. cb_pos.pack(fill="x", padx=20, pady=2)
  791. if current_pos_name in pos_names:
  792. cb_pos.current(pos_names.index(current_pos_name))
  793. ttk.Label(dialog, text="Дата рождения (ГГГГ-ММ-ДД):").pack(anchor="w", padx=20, pady=2)
  794. ent_birth = ttk.Entry(dialog)
  795. ent_birth.pack(fill="x", padx=20, pady=2)
  796. ent_birth.insert(0, birth)
  797. ttk.Label(dialog, text="Серия паспорта:").pack(anchor="w", padx=20, pady=2)
  798. ent_pass_ser = ttk.Entry(dialog)
  799. ent_pass_ser.pack(fill="x", padx=20, pady=2)
  800. ent_pass_ser.insert(0, ps_val)
  801. ttk.Label(dialog, text="Номер паспорта:").pack(anchor="w", padx=20, pady=2)
  802. ent_pass_num = ttk.Entry(dialog)
  803. ent_pass_num.pack(fill="x", padx=20, pady=2)
  804. ent_pass_num.insert(0, pn_val)
  805. ttk.Label(dialog, text="Телефон:").pack(anchor="w", padx=20, pady=2)
  806. ent_phone = ttk.Entry(dialog)
  807. ent_phone.pack(fill="x", padx=20, pady=2)
  808. ent_phone.insert(0, phone)
  809. ttk.Label(dialog, text="Оклад:").pack(anchor="w", padx=20, pady=2)
  810. ent_salary = ttk.Entry(dialog)
  811. ent_salary.pack(fill="x", padx=20, pady=2)
  812. ent_salary.insert(0, str(salary))
  813. def update():
  814. s = ent_surname.get().strip()
  815. n = ent_name.get().strip()
  816. p = ent_patr.get().strip()
  817. b = ent_birth.get().strip()
  818. ps = ent_pass_ser.get().strip()
  819. pn = ent_pass_num.get().strip()
  820. ph = ent_phone.get().strip()
  821. sal_str = ent_salary.get().strip()
  822. if not s or not n or not b or not ps or not pn or not sal_str:
  823. messagebox.showerror("Ошибка", "Заполните обязательные поля!", parent=dialog)
  824. return
  825. try:
  826. sal = float(sal_str)
  827. except ValueError:
  828. messagebox.showerror("Ошибка", "Оклад должен быть числом!", parent=dialog)
  829. return
  830. position_id = positions_list[cb_pos.current()][0]
  831. try:
  832. conn = self.connect_db()
  833. with conn:
  834. with conn.cursor() as cur:
  835. cur.execute("""
  836. UPDATE Personnel
  837. SET surname=%s, name=%s, patronymic=%s, birth_date=%s, passport_series=%s, passport_number=%s, phone=%s, salary=%s, position_id=%s
  838. WHERE employee_id=%s;
  839. """, (s, n, p if p else None, b, ps, pn, ph if ph else None, sal, position_id, emp_id))
  840. conn.close()
  841. self.load_personnel_data()
  842. self.load_reservations_data()
  843. dialog.destroy()
  844. messagebox.showinfo("Успех", "Данные обновлены!")
  845. except Exception as e:
  846. messagebox.showerror("Ошибка СУБД", f"Не удалось обновить: {e}", parent=dialog)
  847. ttk.Button(dialog, text="🔄 Обновить анкету", command=update).pack(pady=15)
  848. def delete_personnel(self):
  849. selected = self.pers_tree.selection()
  850. if not selected:
  851. messagebox.showwarning("Внимание", "Выберите сотрудника для увольнения!")
  852. return
  853. emp_id, fio = self.pers_tree.item(selected[0])['values'][0], self.pers_tree.item(selected[0])['values'][1]
  854. if messagebox.askyesno("Подтверждение", f"Удалить сотрудника '{fio}' из системы?\nЭто очистит связанные записи!"):
  855. try:
  856. conn = self.connect_db()
  857. with conn:
  858. with conn.cursor() as cur:
  859. cur.execute('DELETE FROM Reservations WHERE employee_id = %s;', (emp_id,))
  860. cur.execute('DELETE FROM Personnel WHERE employee_id = %s;', (emp_id,))
  861. conn.close()
  862. self.load_personnel_data()
  863. self.load_reservations_data()
  864. messagebox.showinfo("Успех", f"Сотрудник '{fio}' удален.")
  865. except Exception as e:
  866. messagebox.showerror("Ошибка СУБД", f"Не удалось удалить:\n{e}")
  867. # ==========================================
  868. # УНИВЕРСАЛЬНЫЙ МОДУЛЬ ЭКСПОРТА
  869. # ==========================================
  870. def export_to_csv(self, tree, filename):
  871. headers = [tree.heading(col)["text"] for col in tree["columns"]]
  872. rows = [tree.item(item)["values"] for item in tree.get_children()]
  873. try:
  874. with open(filename, mode="w", newline="", encoding="utf-8-sig") as f:
  875. writer = csv.writer(f, delimiter=";")
  876. writer.writerow(headers)
  877. writer.writerows(rows)
  878. messagebox.showinfo("Успех", f"Данные экспортированы в:\n{os.path.abspath(filename)}")
  879. except Exception as e:
  880. messagebox.showerror("Ошибка экспорта", f"Не удалось сохранить: {e}")
  881. if __name__ == "__main__":
  882. app = BookWarehouseApp()
  883. app.mainloop()