Bladeren bron

Первая версия программы

V4d1ty 1 week geleden
bovenliggende
commit
f98a30a764
4 gewijzigde bestanden met toevoegingen van 651 en 0 verwijderingen
  1. 3 0
      config.py
  2. 140 0
      db.py
  3. 473 0
      gui.py
  4. 35 0
      main.py

+ 3 - 0
config.py

@@ -0,0 +1,3 @@
+DB_HOST = "edu-pg.itiscaf.ru"
+DB_PORT = "5432"
+DB_NAME = "dbwork"

+ 140 - 0
db.py

@@ -0,0 +1,140 @@
+import psycopg2
+from config import DB_HOST, DB_PORT, DB_NAME
+
+class Database:
+    def __init__(self):
+        self.conn = None
+
+    def connect(self, username, password):
+        self.conn = psycopg2.connect(
+            host=DB_HOST,
+            port=DB_PORT,
+            dbname=DB_NAME,
+            user=username,
+            password=password
+        )
+        return self.conn
+
+    def execute(self, query, params=None, fetch=False):
+        try:
+            with self.conn.cursor() as cur:
+                cur.execute(query, params)
+                if fetch:
+                    return cur.fetchall()
+                self.conn.commit()
+                return True
+        except Exception as e:
+            self.conn.rollback()
+            raise e
+
+    #  Отделы 
+    def get_departments(self):
+        return self.execute(
+            "SELECT department_id, head_last_name, head_first_name, head_middle_name, email, details "
+            "FROM accounting_department ORDER BY department_id", fetch=True
+        )
+
+    def get_departments_list(self):
+        rows = self.execute(
+            "SELECT department_id, head_last_name, head_first_name FROM accounting_department ORDER BY department_id",
+            fetch=True
+        )
+        return [(row[0], f"{row[0]}: {row[1]} {row[2]}") for row in rows]
+
+    #  Пользователи 
+    def _get_next_user_id(self):
+        rows = self.execute("SELECT user_id FROM users ORDER BY user_id", fetch=True)
+        existing = {row[0] for row in rows}
+        next_id = 1
+        while next_id in existing:
+            next_id += 1
+        return next_id
+
+    def get_users(self):
+        return self.execute(
+            "SELECT user_id, last_name, first_name, middle_name, birth_date, phone "
+            "FROM users ORDER BY user_id", fetch=True
+        )
+
+    def get_users_list(self):
+        rows = self.execute(
+            "SELECT user_id, last_name, first_name FROM users ORDER BY user_id",
+            fetch=True
+        )
+        return [(row[0], f"{row[0]}: {row[1]} {row[2]}") for row in rows]
+
+    def add_user(self, last_name, first_name, middle_name, birth_date, phone):
+        new_id = self._get_next_user_id()
+        self.execute(
+            "INSERT INTO users (user_id, last_name, first_name, middle_name, birth_date, phone) "
+            "VALUES (%s, %s, %s, %s, %s, %s)",
+            (new_id, last_name, first_name, middle_name, birth_date, phone)
+        )
+
+    def update_user(self, user_id, last_name, first_name, middle_name, birth_date, phone):
+        self.execute(
+            "UPDATE users SET last_name=%s, first_name=%s, middle_name=%s, birth_date=%s, phone=%s "
+            "WHERE user_id=%s",
+            (last_name, first_name, middle_name, birth_date, phone, user_id)
+        )
+
+    def delete_user(self, user_id):
+        self.execute("DELETE FROM users WHERE user_id=%s", (user_id,))
+
+    #  Билеты (представление tickets_full_price) 
+    def _get_next_ticket_id(self):
+        rows = self.execute("SELECT ticket_id FROM tickets ORDER BY ticket_id", fetch=True)
+        existing = {row[0] for row in rows}
+        next_id = 1
+        while next_id in existing:
+            next_id += 1
+        return next_id
+
+    def get_tickets(self): 
+        return self.execute(
+            "SELECT ticket_id, department_id, user_id, performance_name, ticket_price, hall_seat, event_datetime, full_price "
+            "FROM tickets_full_price ORDER BY ticket_id", fetch=True
+        )
+
+    def add_ticket(self, department_id, user_id, performance_name, ticket_price, hall_seat, event_datetime):
+        new_id = self._get_next_ticket_id()
+        self.execute(
+            "INSERT INTO tickets (ticket_id, department_id, user_id, performance_name, ticket_price, hall_seat, event_datetime) "
+            "VALUES (%s, %s, %s, %s, %s, %s, %s)",
+            (new_id, department_id, user_id, performance_name, ticket_price, hall_seat, event_datetime)
+        )
+
+    def update_ticket(self, ticket_id, department_id, user_id, performance_name, ticket_price, hall_seat, event_datetime):
+        self.execute(
+            "UPDATE tickets SET department_id=%s, user_id=%s, performance_name=%s, ticket_price=%s, hall_seat=%s, event_datetime=%s "
+            "WHERE ticket_id=%s",
+            (department_id, user_id, performance_name, ticket_price, hall_seat, event_datetime, ticket_id)
+        )
+
+    def delete_ticket(self, ticket_id):
+        self.execute("DELETE FROM tickets WHERE ticket_id=%s", (ticket_id,))
+
+    #  Поиск 
+    def search_tickets(self, last_name=None, performance=None):
+        sql = """
+            SELECT t.ticket_id, u.last_name, u.first_name, t.performance_name,
+                   t.ticket_price, t.hall_seat, t.event_datetime,
+                   ROUND(t.ticket_price * 1.10, 2) AS full_price
+            FROM tickets t
+            JOIN users u ON t.user_id = u.user_id
+            WHERE 1=1
+        """
+        params = []
+        if last_name:
+            sql += " AND u.last_name ILIKE %s"
+            params.append(f"%{last_name}%")
+        if performance:
+            sql += " AND t.performance_name ILIKE %s"
+            params.append(f"%{performance}%")
+        sql += " ORDER BY t.event_datetime"
+        return self.execute(sql, params, fetch=True)
+
+    #  Отчёт 
+    def revenue_by_performance(self, performance_name):
+        result = self.execute("SELECT get_revenue(%s)", (performance_name,), fetch=True)
+        return result[0][0] if result else 0

+ 473 - 0
gui.py

@@ -0,0 +1,473 @@
+import tkinter as tk
+from tkinter import ttk, messagebox, filedialog
+from datetime import datetime
+import csv
+from db import Database
+from tkcalendar import DateEntry
+
+class TheatreApp:
+    def __init__(self, db):
+        self.db = db
+        self.root = tk.Tk()
+        self.root.title("Театральная система")
+        self.root.geometry("1100x650")
+        self.create_widgets()
+        self.refresh_all()
+
+    def create_widgets(self):
+        notebook = ttk.Notebook(self.root)
+        notebook.pack(fill=tk.BOTH, expand=True)
+
+        # Отделы
+        self.depts_frame = ttk.Frame(notebook)
+        notebook.add(self.depts_frame, text="Отделы")
+        self.depts_tree = self._make_table(self.depts_frame,
+            ["ID","Фамилия","Имя","Отчество","Email","Реквизиты"])
+        tk.Button(self.depts_frame, text="Обновить", command=self.refresh_departments).pack(pady=5)
+
+        # Пользователи
+        self.users_frame = ttk.Frame(notebook)
+        notebook.add(self.users_frame, text="Пользователи")
+        self.users_tree = self._make_table(self.users_frame,
+            ["ID","Фамилия","Имя","Отчество","Дата рождения","Телефон"])
+        btn_frame = tk.Frame(self.users_frame)
+        btn_frame.pack(pady=5)
+        tk.Button(btn_frame, text="Добавить", command=self.add_user).pack(side=tk.LEFT, padx=5)
+        tk.Button(btn_frame, text="Редактировать", command=self.edit_user).pack(side=tk.LEFT, padx=5)
+        tk.Button(btn_frame, text="Удалить", command=self.delete_user).pack(side=tk.LEFT, padx=5)
+        tk.Button(btn_frame, text="Обновить", command=self.refresh_users).pack(side=tk.LEFT, padx=5)
+
+        # Билеты
+        self.tickets_frame = ttk.Frame(notebook)
+        notebook.add(self.tickets_frame, text="Билеты")
+        self.tickets_tree = self._make_table(self.tickets_frame,
+            ["ID","Отдел","Пользователь","Спектакль","Цена","Место","Дата/время","Полная стоимость"])
+        btn_frame2 = tk.Frame(self.tickets_frame)
+        btn_frame2.pack(pady=5)
+        tk.Button(btn_frame2, text="Добавить", command=self.add_ticket).pack(side=tk.LEFT, padx=5)
+        tk.Button(btn_frame2, text="Редактировать", command=self.edit_ticket).pack(side=tk.LEFT, padx=5)
+        tk.Button(btn_frame2, text="Удалить", command=self.delete_ticket).pack(side=tk.LEFT, padx=5)
+        tk.Button(btn_frame2, text="Поиск", command=self.search_tickets).pack(side=tk.LEFT, padx=5)
+        tk.Button(btn_frame2, text="Отчёт (выручка)", command=self.revenue_report).pack(side=tk.LEFT, padx=5)
+        tk.Button(btn_frame2, text="Обновить", command=self.refresh_tickets).pack(side=tk.LEFT, padx=5)
+
+    def _make_table(self, parent, columns):
+        tree = ttk.Treeview(parent, columns=columns, show="headings", height=20)
+        for col in columns:
+            tree.heading(col, text=col)
+            tree.column(col, width=120, anchor="center")
+        tree.pack(fill=tk.BOTH, expand=True)
+        return tree
+
+    def _fill_tree(self, tree, rows):
+        for item in tree.get_children():
+            tree.delete(item)
+        for row in rows:
+            tree.insert("", "end", values=row)
+
+    def refresh_departments(self):
+        rows = self.db.get_departments()
+        self._fill_tree(self.depts_tree, rows)
+
+    def refresh_users(self):
+        rows = self.db.get_users()
+        self._fill_tree(self.users_tree, rows)
+
+    def refresh_tickets(self):
+        rows = self.db.get_tickets()
+        self._fill_tree(self.tickets_tree, rows)
+
+    def add_user(self):
+        dialog = UserDialog(self.root, self.db, title="Добавить пользователя")
+        self.wait_for_dialog(dialog, self.refresh_users)
+
+    def edit_user(self):
+        selected = self.users_tree.selection()
+        if not selected:
+            messagebox.showerror("Ошибка", "Выберите пользователя")
+            return
+        values = self.users_tree.item(selected[0])["values"]
+        user_id = values[0]
+        defaults = {
+            "last_name": values[1],
+            "first_name": values[2],
+            "middle_name": values[3] if values[3] else "",
+            "birth_date": values[4],
+            "phone": values[5]
+        }
+        dialog = UserDialog(self.root, self.db, user_id=user_id, defaults=defaults, title="Редактировать пользователя")
+        self.wait_for_dialog(dialog, self.refresh_users)
+
+    def delete_user(self):
+        selected = self.users_tree.selection()
+        if not selected:
+            messagebox.showerror("Ошибка", "Выберите пользователя")
+            return
+        user_id = self.users_tree.item(selected[0])["values"][0]
+        if messagebox.askyesno("Удаление", f"Удалить пользователя ID={user_id}?"):
+            self.db.delete_user(user_id)
+            self.refresh_users()
+
+    def add_ticket(self):
+        dialog = TicketDialog(self.root, self.db, title="Добавить билет")
+        self.wait_for_dialog(dialog, self.refresh_tickets)
+
+    def edit_ticket(self):
+        selected = self.tickets_tree.selection()
+        if not selected:
+            messagebox.showerror("Ошибка", "Выберите билет")
+            return
+        values = self.tickets_tree.item(selected[0])["values"]
+        ticket_id = values[0]
+        defaults = {
+            "department_id": values[1],
+            "user_id": values[2],
+            "performance_name": values[3],
+            "ticket_price": values[4],
+            "hall_seat": values[5],
+            "event_datetime": values[6]
+        }
+        dialog = TicketDialog(self.root, self.db, ticket_id=ticket_id, defaults=defaults, title="Редактировать билет")
+        self.wait_for_dialog(dialog, self.refresh_tickets)
+
+    def delete_ticket(self):
+        selected = self.tickets_tree.selection()
+        if not selected:
+            messagebox.showerror("Ошибка", "Выберите билет")
+            return
+        ticket_id = self.tickets_tree.item(selected[0])["values"][0]
+        if messagebox.askyesno("Удаление", f"Удалить билет ID={ticket_id}?"):
+            self.db.delete_ticket(ticket_id)
+            self.refresh_tickets()
+
+    def search_tickets(self):
+        SearchDialog(self.root, self.db)
+
+    def revenue_report(self):
+        perf = tk.simpledialog.askstring("Отчёт", "Введите название спектакля:")
+        if perf:
+            total = self.db.revenue_by_performance(perf)
+            messagebox.showinfo("Выручка", f"Выручка по спектаклю '{perf}': {total} руб.")
+
+    def refresh_all(self):
+        self.refresh_departments()
+        self.refresh_users()
+        self.refresh_tickets()
+
+    def wait_for_dialog(self, dialog, refresh_callback):
+        self.root.wait_window(dialog.top)
+        refresh_callback()
+
+    def run(self):
+        self.root.mainloop()
+
+
+#  ДИАЛОГ ПОЛЬЗОВАТЕЛЯ (календарь для даты рождения) 
+class UserDialog:
+    def __init__(self, parent, db, user_id=None, defaults=None, title="Пользователь"):
+        self.db = db
+        self.user_id = user_id
+        self.top = tk.Toplevel(parent)
+        self.top.title(title)
+        self.top.grab_set()
+        self.top.resizable(False, False)
+
+        row = 0
+        tk.Label(self.top, text="Фамилия:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
+        self.last_name_entry = tk.Entry(self.top, width=30)
+        self.last_name_entry.grid(row=row, column=1, padx=5, pady=5)
+        row += 1
+
+        tk.Label(self.top, text="Имя:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
+        self.first_name_entry = tk.Entry(self.top, width=30)
+        self.first_name_entry.grid(row=row, column=1, padx=5, pady=5)
+        row += 1
+
+        tk.Label(self.top, text="Отчество:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
+        self.middle_name_entry = tk.Entry(self.top, width=30)
+        self.middle_name_entry.grid(row=row, column=1, padx=5, pady=5)
+        row += 1
+
+        tk.Label(self.top, text="Дата рождения:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
+        frame_birth = tk.Frame(self.top)
+        frame_birth.grid(row=row, column=1, padx=5, pady=5, sticky="w")
+        self.birth_date = DateEntry(frame_birth, width=12, background='darkblue',
+                                    foreground='white', borderwidth=2,
+                                    date_pattern='yyyy-mm-dd')
+        self.birth_date.pack(side=tk.LEFT)
+        tk.Button(frame_birth, text="Сегодня", command=self.set_today_birth).pack(side=tk.LEFT, padx=5)
+        row += 1
+
+        tk.Label(self.top, text="Телефон:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
+        self.phone_entry = tk.Entry(self.top, width=30)
+        self.phone_entry.grid(row=row, column=1, padx=5, pady=5)
+        row += 1
+
+        btn_frame = tk.Frame(self.top)
+        btn_frame.grid(row=row, column=0, columnspan=2, pady=10)
+        tk.Button(btn_frame, text="Сохранить", command=self.save).pack(side=tk.LEFT, padx=5)
+        tk.Button(btn_frame, text="Отмена", command=self.top.destroy).pack(side=tk.LEFT, padx=5)
+
+        if defaults:
+            self.last_name_entry.insert(0, defaults.get("last_name", ""))
+            self.first_name_entry.insert(0, defaults.get("first_name", ""))
+            self.middle_name_entry.insert(0, defaults.get("middle_name", ""))
+            self.phone_entry.insert(0, defaults.get("phone", ""))
+            birth = defaults.get("birth_date")
+            if birth:
+                try:
+                    if isinstance(birth, datetime):
+                        self.birth_date.set_date(birth)
+                    else:
+                        self.birth_date.set_date(datetime.strptime(str(birth), "%Y-%m-%d"))
+                except:
+                    pass
+        else:
+            self.birth_date.set_date(datetime.now())
+
+    def set_today_birth(self):
+        self.birth_date.set_date(datetime.now())
+
+    def save(self):
+        last = self.last_name_entry.get().strip()
+        first = self.first_name_entry.get().strip()
+        middle = self.middle_name_entry.get().strip() or None
+        phone = self.phone_entry.get().strip()
+        birth_date = self.birth_date.get_date().strftime("%Y-%m-%d")
+
+        if not last or not first or not phone:
+            messagebox.showerror("Ошибка", "Заполните фамилию, имя и телефон")
+            return
+
+        if self.user_id:
+            self.db.update_user(self.user_id, last, first, middle, birth_date, phone)
+        else:
+            self.db.add_user(last, first, middle, birth_date, phone)
+        self.top.destroy()
+
+
+# ДИАЛОГ БИЛЕТА 
+class TicketDialog:
+    def __init__(self, parent, db, ticket_id=None, defaults=None, title="Билет"):
+        self.db = db
+        self.ticket_id = ticket_id
+        self.top = tk.Toplevel(parent)
+        self.top.title(title)
+        self.top.grab_set()
+        self.top.resizable(False, False)
+
+        self.dept_list = db.get_departments_list()
+        self.user_list = db.get_users_list()
+
+        self.dept_var = tk.StringVar()
+        self.user_var = tk.StringVar()
+        self.perf_var = tk.StringVar()
+        self.price_var = tk.StringVar()
+        self.seat_var = tk.StringVar()
+        self.date_var = tk.StringVar()
+        self.hour_var = tk.StringVar()
+        self.minute_var = tk.StringVar()
+
+        row = 0
+        tk.Label(self.top, text="Отдел:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
+        dept_combo = ttk.Combobox(self.top, textvariable=self.dept_var,
+                                  values=[t for _, t in self.dept_list], width=40)
+        dept_combo.grid(row=row, column=1, padx=5, pady=5)
+        row += 1
+
+        tk.Label(self.top, text="Пользователь:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
+        user_combo = ttk.Combobox(self.top, textvariable=self.user_var,
+                                  values=[t for _, t in self.user_list], width=40)
+        user_combo.grid(row=row, column=1, padx=5, pady=5)
+        row += 1
+
+        tk.Label(self.top, text="Спектакль:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
+        tk.Entry(self.top, textvariable=self.perf_var, width=42).grid(row=row, column=1, padx=5, pady=5)
+        row += 1
+
+        tk.Label(self.top, text="Цена:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
+        tk.Entry(self.top, textvariable=self.price_var, width=42).grid(row=row, column=1, padx=5, pady=5)
+        row += 1
+
+        tk.Label(self.top, text="Место:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
+        tk.Entry(self.top, textvariable=self.seat_var, width=42).grid(row=row, column=1, padx=5, pady=5)
+        row += 1
+
+        # Дата и время
+        tk.Label(self.top, text="Дата:").grid(row=row, column=0, padx=5, pady=5, sticky="e")
+        self.date_picker = DateEntry(self.top, width=12, background='darkblue',
+                                     foreground='white', borderwidth=2,
+                                     date_pattern='yyyy-mm-dd', textvariable=self.date_var)
+        self.date_picker.grid(row=row, column=1, padx=5, pady=5, sticky="w")
+        row += 1
+
+        tk.Label(self.top, text="Час (0-23):").grid(row=row, column=0, padx=5, pady=5, sticky="e")
+        self.hour_spin = tk.Spinbox(self.top, from_=0, to=23, width=5, format="%02.0f", textvariable=self.hour_var)
+        self.hour_spin.grid(row=row, column=1, padx=5, pady=5, sticky="w")
+        row += 1
+
+        tk.Label(self.top, text="Минуты (0-59):").grid(row=row, column=0, padx=5, pady=5, sticky="e")
+        self.minute_spin = tk.Spinbox(self.top, from_=0, to=59, width=5, format="%02.0f", textvariable=self.minute_var)
+        self.minute_spin.grid(row=row, column=1, padx=5, pady=5, sticky="w")
+        row += 1
+
+        def set_now():
+            now = datetime.now()
+            self.date_picker.set_date(now)
+            self.hour_var.set(f"{now.hour:02d}")
+            self.minute_var.set(f"{now.minute:02d}")
+        tk.Button(self.top, text="Сейчас", command=set_now).grid(row=row, column=1, padx=5, pady=5, sticky="w")
+        row += 1
+
+        if defaults:
+            self.set_defaults(defaults)
+        else:
+            self.date_picker.set_date(datetime.now())
+            self.hour_var.set("00")
+            self.minute_var.set("00")
+
+        btn_frame = tk.Frame(self.top)
+        btn_frame.grid(row=row, column=0, columnspan=2, pady=10)
+        tk.Button(btn_frame, text="Сохранить", command=self.save).pack(side=tk.LEFT, padx=5)
+        tk.Button(btn_frame, text="Отмена", command=self.top.destroy).pack(side=tk.LEFT, padx=5)
+
+    def set_defaults(self, defaults):
+        dept_id = defaults.get("department_id")
+        if dept_id:
+            for id_, text in self.dept_list:
+                if id_ == dept_id:
+                    self.dept_var.set(text)
+                    break
+        user_id = defaults.get("user_id")
+        if user_id:
+            for id_, text in self.user_list:
+                if id_ == user_id:
+                    self.user_var.set(text)
+                    break
+        self.perf_var.set(defaults.get("performance_name", ""))
+        self.price_var.set(str(defaults.get("ticket_price", "")))
+        self.seat_var.set(defaults.get("hall_seat", ""))
+        dt = defaults.get("event_datetime")
+        if dt:
+            if isinstance(dt, datetime):
+                self.date_picker.set_date(dt)
+                self.hour_var.set(f"{dt.hour:02d}")
+                self.minute_var.set(f"{dt.minute:02d}")
+            else:
+                try:
+                    dt_obj = datetime.strptime(str(dt), "%Y-%m-%d %H:%M:%S")
+                    self.date_picker.set_date(dt_obj)
+                    self.hour_var.set(f"{dt_obj.hour:02d}")
+                    self.minute_var.set(f"{dt_obj.minute:02d}")
+                except:
+                    pass
+
+    def save(self):
+        dept_text = self.dept_var.get()
+        dept_id = None
+        for id_, text in self.dept_list:
+            if text == dept_text:
+                dept_id = id_
+                break
+        if dept_id is None:
+            messagebox.showerror("Ошибка", "Выберите отдел из списка")
+            return
+
+        user_text = self.user_var.get()
+        user_id = None
+        for id_, text in self.user_list:
+            if text == user_text:
+                user_id = id_
+                break
+        if user_id is None:
+            messagebox.showerror("Ошибка", "Выберите пользователя из списка")
+            return
+
+        perf = self.perf_var.get().strip()
+        if not perf:
+            messagebox.showerror("Ошибка", "Введите название спектакля")
+            return
+
+        try:
+            price = float(self.price_var.get().strip())
+        except ValueError:
+            messagebox.showerror("Ошибка", "Цена должна быть числом")
+            return
+
+        seat = self.seat_var.get().strip()
+        if not seat:
+            messagebox.showerror("Ошибка", "Введите место")
+            return
+
+        date_obj = self.date_picker.get_date()
+        try:
+            hour = int(self.hour_var.get().strip())
+            minute = int(self.minute_var.get().strip())
+        except ValueError:
+            messagebox.showerror("Ошибка", "Время должно быть числом")
+            return
+        if hour < 0 or hour > 23 or minute < 0 or minute > 59:
+            messagebox.showerror("Ошибка", "Некорректное время")
+            return
+        event_dt = datetime(date_obj.year, date_obj.month, date_obj.day, hour, minute)
+
+        if self.ticket_id:
+            self.db.update_ticket(self.ticket_id, dept_id, user_id, perf, price, seat, event_dt)
+        else:
+            self.db.add_ticket(dept_id, user_id, perf, price, seat, event_dt)
+        self.top.destroy()
+
+
+# ДИАЛОГ ПОИСКА 
+class SearchDialog:
+    def __init__(self, parent, db):
+        self.db = db
+        self.top = tk.Toplevel(parent)
+        self.top.title("Поиск билетов")
+        self.top.grab_set()
+        self.top.geometry("900x500")
+
+        frame = tk.Frame(self.top)
+        frame.pack(pady=10)
+        tk.Label(frame, text="Фамилия (часть):").grid(row=0, column=0, padx=5, pady=5)
+        self.last_entry = tk.Entry(frame, width=20)
+        self.last_entry.grid(row=0, column=1, padx=5, pady=5)
+        tk.Label(frame, text="Спектакль (часть):").grid(row=0, column=2, padx=5, pady=5)
+        self.perf_entry = tk.Entry(frame, width=20)
+        self.perf_entry.grid(row=0, column=3, padx=5, pady=5)
+        tk.Button(frame, text="Искать", command=self.search).grid(row=0, column=4, padx=10)
+
+        self.tree = ttk.Treeview(self.top, columns=("ID","Фамилия","Имя","Спектакль","Цена","Место","Дата","Полная"), show="headings")
+        for col in self.tree["columns"]:
+            self.tree.heading(col, text=col)
+            self.tree.column(col, width=100, anchor="center")
+        self.tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
+
+        tk.Button(self.top, text="Экспорт в CSV", command=self.export_csv).pack(pady=5)
+
+    def search(self):
+        last = self.last_entry.get().strip()
+        perf = self.perf_entry.get().strip()
+        rows = self.db.search_tickets(last, perf)
+        for item in self.tree.get_children():
+            self.tree.delete(item)
+        if rows:
+            for row in rows:
+                self.tree.insert("", "end", values=row)
+        else:
+            messagebox.showinfo("Результат", "Ничего не найдено")
+
+    def export_csv(self):
+        if not self.tree.get_children():
+            messagebox.showwarning("Нет данных", "Сначала выполните поиск")
+            return
+        filename = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV", "*.csv")])
+        if filename:
+            data = []
+            for child in self.tree.get_children():
+                data.append(self.tree.item(child)["values"])
+            with open(filename, 'w', encoding='utf-8', newline='') as f:
+                writer = csv.writer(f)
+                writer.writerow(["ID","Фамилия","Имя","Спектакль","Цена","Место","Дата","Полная"])
+                writer.writerows(data)
+            messagebox.showinfo("Успех", f"Экспортировано в {filename}")

+ 35 - 0
main.py

@@ -0,0 +1,35 @@
+import tkinter as tk
+from tkinter import simpledialog, messagebox
+from db import Database
+from gui import TheatreApp
+
+def ask_credentials():
+    root = tk.Tk()
+    root.withdraw()  
+    username = simpledialog.askstring("Авторизация", "Введите имя пользователя БД:", parent=root)
+    if username is None:
+        root.destroy()
+        return None, None
+    password = simpledialog.askstring("Авторизация", "Введите пароль:", parent=root, show='*')
+    root.destroy()
+    if password is None:
+        return None, None
+    return username, password
+
+def main():
+    db = Database()
+    while True:
+        username, password = ask_credentials()
+        if username is None:
+            print("Вход отменён")
+            return
+        try:
+            db.connect(username, password)
+            break
+        except Exception as e:
+            messagebox.showerror("Ошибка подключения", f"Не удалось подключиться:\n{e}\nПроверьте имя пользователя и пароль.")
+    app = TheatreApp(db)
+    app.run()
+
+if __name__ == "__main__":
+    main()