gui.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. import tkinter as tk
  2. from tkinter import ttk, messagebox, filedialog
  3. from datetime import datetime
  4. import csv
  5. from db import Database
  6. from tkcalendar import DateEntry
  7. class TheatreApp:
  8. def __init__(self, db):
  9. self.db = db
  10. self.root = tk.Tk()
  11. self.root.title("Театральная система")
  12. self.root.geometry("1100x650")
  13. self.create_widgets()
  14. self.refresh_all()
  15. def create_widgets(self):
  16. notebook = ttk.Notebook(self.root)
  17. notebook.pack(fill=tk.BOTH, expand=True)
  18. # Отделы
  19. self.depts_frame = ttk.Frame(notebook)
  20. notebook.add(self.depts_frame, text="Отделы")
  21. self.depts_tree = self._make_table(self.depts_frame,
  22. ["ID","Фамилия","Имя","Отчество","Email","Реквизиты"])
  23. tk.Button(self.depts_frame, text="Обновить", command=self.refresh_departments).pack(pady=5)
  24. # Пользователи
  25. self.users_frame = ttk.Frame(notebook)
  26. notebook.add(self.users_frame, text="Пользователи")
  27. self.users_tree = self._make_table(self.users_frame,
  28. ["ID","Фамилия","Имя","Отчество","Дата рождения","Телефон"])
  29. btn_frame = tk.Frame(self.users_frame)
  30. btn_frame.pack(pady=5)
  31. tk.Button(btn_frame, text="Добавить", command=self.add_user).pack(side=tk.LEFT, padx=5)
  32. tk.Button(btn_frame, text="Редактировать", command=self.edit_user).pack(side=tk.LEFT, padx=5)
  33. tk.Button(btn_frame, text="Удалить", command=self.delete_user).pack(side=tk.LEFT, padx=5)
  34. tk.Button(btn_frame, text="Обновить", command=self.refresh_users).pack(side=tk.LEFT, padx=5)
  35. # Билеты
  36. self.tickets_frame = ttk.Frame(notebook)
  37. notebook.add(self.tickets_frame, text="Билеты")
  38. self.tickets_tree = self._make_table(self.tickets_frame,
  39. ["ID","Отдел","Пользователь","Спектакль","Цена","Место","Дата/время","Полная стоимость"])
  40. btn_frame2 = tk.Frame(self.tickets_frame)
  41. btn_frame2.pack(pady=5)
  42. tk.Button(btn_frame2, text="Добавить", command=self.add_ticket).pack(side=tk.LEFT, padx=5)
  43. tk.Button(btn_frame2, text="Редактировать", command=self.edit_ticket).pack(side=tk.LEFT, padx=5)
  44. tk.Button(btn_frame2, text="Удалить", command=self.delete_ticket).pack(side=tk.LEFT, padx=5)
  45. tk.Button(btn_frame2, text="Поиск", command=self.search_tickets).pack(side=tk.LEFT, padx=5)
  46. tk.Button(btn_frame2, text="Отчёт (выручка)", command=self.revenue_report).pack(side=tk.LEFT, padx=5)
  47. tk.Button(btn_frame2, text="Обновить", command=self.refresh_tickets).pack(side=tk.LEFT, padx=5)
  48. def _make_table(self, parent, columns):
  49. tree = ttk.Treeview(parent, columns=columns, show="headings", height=20)
  50. for col in columns:
  51. tree.heading(col, text=col)
  52. tree.column(col, width=120, anchor="center")
  53. tree.pack(fill=tk.BOTH, expand=True)
  54. return tree
  55. def _fill_tree(self, tree, rows):
  56. for item in tree.get_children():
  57. tree.delete(item)
  58. for row in rows:
  59. tree.insert("", "end", values=row)
  60. def refresh_departments(self):
  61. rows = self.db.get_departments()
  62. self._fill_tree(self.depts_tree, rows)
  63. def refresh_users(self):
  64. rows = self.db.get_users()
  65. self._fill_tree(self.users_tree, rows)
  66. def refresh_tickets(self):
  67. rows = self.db.get_tickets()
  68. self._fill_tree(self.tickets_tree, rows)
  69. def add_user(self):
  70. dialog = UserDialog(self.root, self.db, title="Добавить пользователя")
  71. self.wait_for_dialog(dialog, self.refresh_users)
  72. def edit_user(self):
  73. selected = self.users_tree.selection()
  74. if not selected:
  75. messagebox.showerror("Ошибка", "Выберите пользователя")
  76. return
  77. values = self.users_tree.item(selected[0])["values"]
  78. user_id = values[0]
  79. defaults = {
  80. "last_name": values[1],
  81. "first_name": values[2],
  82. "middle_name": values[3] if values[3] else "",
  83. "birth_date": values[4],
  84. "phone": values[5]
  85. }
  86. dialog = UserDialog(self.root, self.db, user_id=user_id, defaults=defaults, title="Редактировать пользователя")
  87. self.wait_for_dialog(dialog, self.refresh_users)
  88. def delete_user(self):
  89. selected = self.users_tree.selection()
  90. if not selected:
  91. messagebox.showerror("Ошибка", "Выберите пользователя")
  92. return
  93. user_id = self.users_tree.item(selected[0])["values"][0]
  94. if messagebox.askyesno("Удаление", f"Удалить пользователя ID={user_id}?"):
  95. self.db.delete_user(user_id)
  96. self.refresh_users()
  97. def add_ticket(self):
  98. dialog = TicketDialog(self.root, self.db, title="Добавить билет")
  99. self.wait_for_dialog(dialog, self.refresh_tickets)
  100. def edit_ticket(self):
  101. selected = self.tickets_tree.selection()
  102. if not selected:
  103. messagebox.showerror("Ошибка", "Выберите билет")
  104. return
  105. values = self.tickets_tree.item(selected[0])["values"]
  106. ticket_id = values[0]
  107. defaults = {
  108. "department_id": values[1],
  109. "user_id": values[2],
  110. "performance_name": values[3],
  111. "ticket_price": values[4],
  112. "hall_seat": values[5],
  113. "event_datetime": values[6]
  114. }
  115. dialog = TicketDialog(self.root, self.db, ticket_id=ticket_id, defaults=defaults, title="Редактировать билет")
  116. self.wait_for_dialog(dialog, self.refresh_tickets)
  117. def delete_ticket(self):
  118. selected = self.tickets_tree.selection()
  119. if not selected:
  120. messagebox.showerror("Ошибка", "Выберите билет")
  121. return
  122. ticket_id = self.tickets_tree.item(selected[0])["values"][0]
  123. if messagebox.askyesno("Удаление", f"Удалить билет ID={ticket_id}?"):
  124. self.db.delete_ticket(ticket_id)
  125. self.refresh_tickets()
  126. def search_tickets(self):
  127. SearchDialog(self.root, self.db)
  128. def revenue_report(self):
  129. perf = tk.simpledialog.askstring("Отчёт", "Введите название спектакля:")
  130. if perf:
  131. total = self.db.revenue_by_performance(perf)
  132. messagebox.showinfo("Выручка", f"Выручка по спектаклю '{perf}': {total} руб.")
  133. def refresh_all(self):
  134. self.refresh_departments()
  135. self.refresh_users()
  136. self.refresh_tickets()
  137. def wait_for_dialog(self, dialog, refresh_callback):
  138. self.root.wait_window(dialog.top)
  139. refresh_callback()
  140. def run(self):
  141. self.root.mainloop()
  142. # ДИАЛОГ ПОЛЬЗОВАТЕЛЯ (календарь для даты рождения)
  143. class UserDialog:
  144. def __init__(self, parent, db, user_id=None, defaults=None, title="Пользователь"):
  145. self.db = db
  146. self.user_id = user_id
  147. self.top = tk.Toplevel(parent)
  148. self.top.title(title)
  149. self.top.grab_set()
  150. self.top.resizable(False, False)
  151. row = 0
  152. tk.Label(self.top, text="Фамилия:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
  153. self.last_name_entry = tk.Entry(self.top, width=30)
  154. self.last_name_entry.grid(row=row, column=1, padx=5, pady=5)
  155. row += 1
  156. tk.Label(self.top, text="Имя:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
  157. self.first_name_entry = tk.Entry(self.top, width=30)
  158. self.first_name_entry.grid(row=row, column=1, padx=5, pady=5)
  159. row += 1
  160. tk.Label(self.top, text="Отчество:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
  161. self.middle_name_entry = tk.Entry(self.top, width=30)
  162. self.middle_name_entry.grid(row=row, column=1, padx=5, pady=5)
  163. row += 1
  164. tk.Label(self.top, text="Дата рождения:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
  165. frame_birth = tk.Frame(self.top)
  166. frame_birth.grid(row=row, column=1, padx=5, pady=5, sticky="w")
  167. self.birth_date = DateEntry(frame_birth, width=12, background='darkblue',
  168. foreground='white', borderwidth=2,
  169. date_pattern='yyyy-mm-dd')
  170. self.birth_date.pack(side=tk.LEFT)
  171. tk.Button(frame_birth, text="Сегодня", command=self.set_today_birth).pack(side=tk.LEFT, padx=5)
  172. row += 1
  173. tk.Label(self.top, text="Телефон:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
  174. self.phone_entry = tk.Entry(self.top, width=30)
  175. self.phone_entry.grid(row=row, column=1, padx=5, pady=5)
  176. row += 1
  177. btn_frame = tk.Frame(self.top)
  178. btn_frame.grid(row=row, column=0, columnspan=2, pady=10)
  179. tk.Button(btn_frame, text="Сохранить", command=self.save).pack(side=tk.LEFT, padx=5)
  180. tk.Button(btn_frame, text="Отмена", command=self.top.destroy).pack(side=tk.LEFT, padx=5)
  181. if defaults:
  182. self.last_name_entry.insert(0, defaults.get("last_name", ""))
  183. self.first_name_entry.insert(0, defaults.get("first_name", ""))
  184. self.middle_name_entry.insert(0, defaults.get("middle_name", ""))
  185. self.phone_entry.insert(0, defaults.get("phone", ""))
  186. birth = defaults.get("birth_date")
  187. if birth:
  188. try:
  189. if isinstance(birth, datetime):
  190. self.birth_date.set_date(birth)
  191. else:
  192. self.birth_date.set_date(datetime.strptime(str(birth), "%Y-%m-%d"))
  193. except:
  194. pass
  195. else:
  196. self.birth_date.set_date(datetime.now())
  197. def set_today_birth(self):
  198. self.birth_date.set_date(datetime.now())
  199. def save(self):
  200. last = self.last_name_entry.get().strip()
  201. first = self.first_name_entry.get().strip()
  202. middle = self.middle_name_entry.get().strip() or None
  203. phone = self.phone_entry.get().strip()
  204. birth_date = self.birth_date.get_date().strftime("%Y-%m-%d")
  205. if not last or not first or not phone:
  206. messagebox.showerror("Ошибка", "Заполните фамилию, имя и телефон")
  207. return
  208. if self.user_id:
  209. self.db.update_user(self.user_id, last, first, middle, birth_date, phone)
  210. else:
  211. self.db.add_user(last, first, middle, birth_date, phone)
  212. self.top.destroy()
  213. # ДИАЛОГ БИЛЕТА
  214. class TicketDialog:
  215. def __init__(self, parent, db, ticket_id=None, defaults=None, title="Билет"):
  216. self.db = db
  217. self.ticket_id = ticket_id
  218. self.top = tk.Toplevel(parent)
  219. self.top.title(title)
  220. self.top.grab_set()
  221. self.top.resizable(False, False)
  222. self.dept_list = db.get_departments_list()
  223. self.user_list = db.get_users_list()
  224. self.dept_var = tk.StringVar()
  225. self.user_var = tk.StringVar()
  226. self.perf_var = tk.StringVar()
  227. self.price_var = tk.StringVar()
  228. self.seat_var = tk.StringVar()
  229. self.date_var = tk.StringVar()
  230. self.hour_var = tk.StringVar()
  231. self.minute_var = tk.StringVar()
  232. row = 0
  233. tk.Label(self.top, text="Отдел:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
  234. dept_combo = ttk.Combobox(self.top, textvariable=self.dept_var,
  235. values=[t for _, t in self.dept_list], width=40)
  236. dept_combo.grid(row=row, column=1, padx=5, pady=5)
  237. row += 1
  238. tk.Label(self.top, text="Пользователь:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
  239. user_combo = ttk.Combobox(self.top, textvariable=self.user_var,
  240. values=[t for _, t in self.user_list], width=40)
  241. user_combo.grid(row=row, column=1, padx=5, pady=5)
  242. row += 1
  243. tk.Label(self.top, text="Спектакль:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
  244. tk.Entry(self.top, textvariable=self.perf_var, width=42).grid(row=row, column=1, padx=5, pady=5)
  245. row += 1
  246. tk.Label(self.top, text="Цена:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
  247. tk.Entry(self.top, textvariable=self.price_var, width=42).grid(row=row, column=1, padx=5, pady=5)
  248. row += 1
  249. tk.Label(self.top, text="Место:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
  250. tk.Entry(self.top, textvariable=self.seat_var, width=42).grid(row=row, column=1, padx=5, pady=5)
  251. row += 1
  252. # Дата и время
  253. tk.Label(self.top, text="Дата:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
  254. self.date_picker = DateEntry(self.top, width=12, background='darkblue',
  255. foreground='white', borderwidth=2,
  256. date_pattern='yyyy-mm-dd', textvariable=self.date_var)
  257. self.date_picker.grid(row=row, column=1, padx=5, pady=5, sticky="w")
  258. row += 1
  259. tk.Label(self.top, text="Час (0-23):").grid(row=row, column=0, padx=5, pady=5, sticky="e")
  260. self.hour_spin = tk.Spinbox(self.top, from_=0, to=23, width=5, format="%02.0f", textvariable=self.hour_var)
  261. self.hour_spin.grid(row=row, column=1, padx=5, pady=5, sticky="w")
  262. row += 1
  263. tk.Label(self.top, text="Минуты (0-59):").grid(row=row, column=0, padx=5, pady=5, sticky="e")
  264. self.minute_spin = tk.Spinbox(self.top, from_=0, to=59, width=5, format="%02.0f", textvariable=self.minute_var)
  265. self.minute_spin.grid(row=row, column=1, padx=5, pady=5, sticky="w")
  266. row += 1
  267. def set_now():
  268. now = datetime.now()
  269. self.date_picker.set_date(now)
  270. self.hour_var.set(f"{now.hour:02d}")
  271. self.minute_var.set(f"{now.minute:02d}")
  272. tk.Button(self.top, text="Сейчас", command=set_now).grid(row=row, column=1, padx=5, pady=5, sticky="w")
  273. row += 1
  274. if defaults:
  275. self.set_defaults(defaults)
  276. else:
  277. self.date_picker.set_date(datetime.now())
  278. self.hour_var.set("00")
  279. self.minute_var.set("00")
  280. btn_frame = tk.Frame(self.top)
  281. btn_frame.grid(row=row, column=0, columnspan=2, pady=10)
  282. tk.Button(btn_frame, text="Сохранить", command=self.save).pack(side=tk.LEFT, padx=5)
  283. tk.Button(btn_frame, text="Отмена", command=self.top.destroy).pack(side=tk.LEFT, padx=5)
  284. def set_defaults(self, defaults):
  285. dept_id = defaults.get("department_id")
  286. if dept_id:
  287. for id_, text in self.dept_list:
  288. if id_ == dept_id:
  289. self.dept_var.set(text)
  290. break
  291. user_id = defaults.get("user_id")
  292. if user_id:
  293. for id_, text in self.user_list:
  294. if id_ == user_id:
  295. self.user_var.set(text)
  296. break
  297. self.perf_var.set(defaults.get("performance_name", ""))
  298. self.price_var.set(str(defaults.get("ticket_price", "")))
  299. self.seat_var.set(defaults.get("hall_seat", ""))
  300. dt = defaults.get("event_datetime")
  301. if dt:
  302. if isinstance(dt, datetime):
  303. self.date_picker.set_date(dt)
  304. self.hour_var.set(f"{dt.hour:02d}")
  305. self.minute_var.set(f"{dt.minute:02d}")
  306. else:
  307. try:
  308. dt_obj = datetime.strptime(str(dt), "%Y-%m-%d %H:%M:%S")
  309. self.date_picker.set_date(dt_obj)
  310. self.hour_var.set(f"{dt_obj.hour:02d}")
  311. self.minute_var.set(f"{dt_obj.minute:02d}")
  312. except:
  313. pass
  314. def save(self):
  315. dept_text = self.dept_var.get()
  316. dept_id = None
  317. for id_, text in self.dept_list:
  318. if text == dept_text:
  319. dept_id = id_
  320. break
  321. if dept_id is None:
  322. messagebox.showerror("Ошибка", "Выберите отдел из списка")
  323. return
  324. user_text = self.user_var.get()
  325. user_id = None
  326. for id_, text in self.user_list:
  327. if text == user_text:
  328. user_id = id_
  329. break
  330. if user_id is None:
  331. messagebox.showerror("Ошибка", "Выберите пользователя из списка")
  332. return
  333. perf = self.perf_var.get().strip()
  334. if not perf:
  335. messagebox.showerror("Ошибка", "Введите название спектакля")
  336. return
  337. try:
  338. price = float(self.price_var.get().strip())
  339. except ValueError:
  340. messagebox.showerror("Ошибка", "Цена должна быть числом")
  341. return
  342. seat = self.seat_var.get().strip()
  343. if not seat:
  344. messagebox.showerror("Ошибка", "Введите место")
  345. return
  346. date_obj = self.date_picker.get_date()
  347. try:
  348. hour = int(self.hour_var.get().strip())
  349. minute = int(self.minute_var.get().strip())
  350. except ValueError:
  351. messagebox.showerror("Ошибка", "Время должно быть числом")
  352. return
  353. if hour < 0 or hour > 23 or minute < 0 or minute > 59:
  354. messagebox.showerror("Ошибка", "Некорректное время")
  355. return
  356. event_dt = datetime(date_obj.year, date_obj.month, date_obj.day, hour, minute)
  357. if self.ticket_id:
  358. self.db.update_ticket(self.ticket_id, dept_id, user_id, perf, price, seat, event_dt)
  359. else:
  360. self.db.add_ticket(dept_id, user_id, perf, price, seat, event_dt)
  361. self.top.destroy()
  362. # ДИАЛОГ ПОИСКА
  363. class SearchDialog:
  364. def __init__(self, parent, db):
  365. self.db = db
  366. self.top = tk.Toplevel(parent)
  367. self.top.title("Поиск билетов")
  368. self.top.grab_set()
  369. self.top.geometry("900x500")
  370. frame = tk.Frame(self.top)
  371. frame.pack(pady=10)
  372. tk.Label(frame, text="Фамилия (часть):").grid(row=0, column=0, padx=5, pady=5)
  373. self.last_entry = tk.Entry(frame, width=20)
  374. self.last_entry.grid(row=0, column=1, padx=5, pady=5)
  375. tk.Label(frame, text="Спектакль (часть):").grid(row=0, column=2, padx=5, pady=5)
  376. self.perf_entry = tk.Entry(frame, width=20)
  377. self.perf_entry.grid(row=0, column=3, padx=5, pady=5)
  378. tk.Button(frame, text="Искать", command=self.search).grid(row=0, column=4, padx=10)
  379. self.tree = ttk.Treeview(self.top, columns=("ID","Фамилия","Имя","Спектакль","Цена","Место","Дата","Полная"), show="headings")
  380. for col in self.tree["columns"]:
  381. self.tree.heading(col, text=col)
  382. self.tree.column(col, width=100, anchor="center")
  383. self.tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
  384. tk.Button(self.top, text="Экспорт в CSV", command=self.export_csv).pack(pady=5)
  385. def search(self):
  386. last = self.last_entry.get().strip()
  387. perf = self.perf_entry.get().strip()
  388. rows = self.db.search_tickets(last, perf)
  389. for item in self.tree.get_children():
  390. self.tree.delete(item)
  391. if rows:
  392. for row in rows:
  393. self.tree.insert("", "end", values=row)
  394. else:
  395. messagebox.showinfo("Результат", "Ничего не найдено")
  396. def export_csv(self):
  397. if not self.tree.get_children():
  398. messagebox.showwarning("Нет данных", "Сначала выполните поиск")
  399. return
  400. filename = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV", "*.csv")])
  401. if filename:
  402. data = []
  403. for child in self.tree.get_children():
  404. data.append(self.tree.item(child)["values"])
  405. with open(filename, 'w', encoding='utf-8', newline='') as f:
  406. writer = csv.writer(f)
  407. writer.writerow(["ID","Фамилия","Имя","Спектакль","Цена","Место","Дата","Полная"])
  408. writer.writerows(data)
  409. messagebox.showinfo("Успех", f"Экспортировано в {filename}")