Загрузка данных


import mysql.connector
from mysql.connector import Error
import tkinter as tk
from tkinter import ttk, messagebox, Toplevel
import re
from datetime import datetime

# ==================== КОНФИГУРАЦИЯ БАЗЫ ДАННЫХ ====================
DB_CONFIG = {
    'host': 'localhost',
    'user': 'root',        # измените при необходимости
    'password': '',        # ваш пароль MySQL
    'database': 'autoservice',
    'port': 3306
}

# ==================== ИНИЦИАЛИЗАЦИЯ БАЗЫ ДАННЫХ ====================
def init_db():
    """Создаёт таблицы, если они не существуют"""
    connection = None
    try:
        connection = mysql.connector.connect(
            host=DB_CONFIG['host'],
            user=DB_CONFIG['user'],
            password=DB_CONFIG['password'],
            port=DB_CONFIG['port']
        )
        cursor = connection.cursor()
        # Создаём БД, если её нет
        cursor.execute(f"CREATE DATABASE IF NOT EXISTS {DB_CONFIG['database']}")
        cursor.execute(f"USE {DB_CONFIG['database']}")

        # Таблица владельцев
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS owners (
                id INT AUTO_INCREMENT PRIMARY KEY,
                full_name VARCHAR(255) NOT NULL,
                phone VARCHAR(50),
                address TEXT
            )
        """)

        # Таблица автомобилей
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS cars (
                id INT AUTO_INCREMENT PRIMARY KEY,
                license_plate VARCHAR(20) NOT NULL UNIQUE,
                brand VARCHAR(100) NOT NULL,
                model VARCHAR(100) NOT NULL,
                year INT,
                color VARCHAR(50),
                owner_id INT,
                FOREIGN KEY (owner_id) REFERENCES owners(id) ON DELETE SET NULL
            )
        """)
        connection.commit()
    except Error as e:
        messagebox.showerror("Ошибка БД", f"Не удалось инициализировать БД:\n{e}")
        return False
    finally:
        if connection and connection.is_connected():
            cursor.close()
            connection.close()
    return True

# ==================== ФУНКЦИИ РАБОТЫ С БАЗОЙ ДАННЫХ ====================
def get_db_connection():
    """Установка соединения с базой данных"""
    try:
        connection = mysql.connector.connect(**DB_CONFIG)
        return connection
    except Error as e:
        messagebox.showerror("Ошибка БД", f"Не удалось подключиться к БД:\n{e}")
        return None

def load_cars_data(tree):
    """Загрузка данных из БД в Treeview (с хранением ID автомобиля)"""
    for row in tree.get_children():
        tree.delete(row)

    connection = get_db_connection()
    if not connection:
        return

    cursor = connection.cursor()
    query = """
        SELECT c.id, c.license_plate, c.brand, c.model, c.year, c.color,
               o.full_name, o.phone, o.address
        FROM cars c
        LEFT JOIN owners o ON c.owner_id = o.id
        ORDER BY c.id
    """
    try:
        cursor.execute(query)
        rows = cursor.fetchall()
        for row in rows:
            # Вставляем ID как скрытое значение (первый столбец)
            tree.insert('', tk.END, values=row, iid=row[0])
    except Error as e:
        messagebox.showerror("Ошибка", f"Ошибка загрузки данных:\n{e}")
    finally:
        cursor.close()
        connection.close()

def find_owner_by_name(full_name):
    """Поиск владельца по ФИО (возвращает id)"""
    connection = get_db_connection()
    if not connection:
        return None
    cursor = connection.cursor()
    query = "SELECT id FROM owners WHERE full_name = %s"
    try:
        cursor.execute(query, (full_name,))
        result = cursor.fetchone()
        return result[0] if result else None
    except Error as e:
        print(f"Ошибка поиска владельца: {e}")
        return None
    finally:
        cursor.close()
        connection.close()



def add_owner(full_name, phone, address):
    """Добавление нового владельца в БД (возвращает id)"""
    connection = get_db_connection()
    if not connection:
        return None
    cursor = connection.cursor()
    query = "INSERT INTO owners (full_name, phone, address) VALUES (%s, %s, %s)"
    try:
        cursor.execute(query, (full_name, phone, address))
        connection.commit()
        return cursor.lastrowid
    except Error as e:
        messagebox.showerror("Ошибка", f"Не удалось добавить владельца:\n{e}")
        return None
    finally:
        cursor.close()
        connection.close()

def update_owner(owner_id, full_name, phone, address):
    """Обновление данных владельца"""
    connection = get_db_connection()
    if not connection:
        return False
    cursor = connection.cursor()
    query = "UPDATE owners SET full_name = %s, phone = %s, address = %s WHERE id = %s"
    try:
        cursor.execute(query, (full_name, phone, address, owner_id))
        connection.commit()
        return True
    except Error as e:
        messagebox.showerror("Ошибка", f"Не удалось обновить владельца:\n{e}")
        return False
    finally:
        cursor.close()
        connection.close()

def add_car(license_plate, brand, model, year, color, owner_id):
    """Добавление нового автомобиля"""
    connection = get_db_connection()
    if not connection:
        return False
    cursor = connection.cursor()
    query = """
        INSERT INTO cars (license_plate, brand, model, year, color, owner_id)
        VALUES (%s, %s, %s, %s, %s, %s)
    """
    try:
        cursor.execute(query, (license_plate, brand, model, year, color, owner_id))
        connection.commit()
        return True
    except Error as e:
        if "Duplicate" in str(e):
            messagebox.showerror("Ошибка", f"Автомобиль с госномером '{license_plate}' уже существует!")
        else:
            messagebox.showerror("Ошибка", f"Не удалось добавить автомобиль:\n{e}")
        return False
    finally:
        cursor.close()
        connection.close()

def update_car(car_id, license_plate, brand, model, year, color, owner_id):
    """Обновление данных автомобиля"""
    connection = get_db_connection()
    if not connection:
        return False
    cursor = connection.cursor()
    query = """
        UPDATE cars
        SET license_plate = %s, brand = %s, model = %s, year = %s, color = %s, owner_id = %s
        WHERE id = %s
    """
    try:
        cursor.execute(query, (license_plate, brand, model, year, color, owner_id, car_id))
        connection.commit()
        return True
    except Error as e:
        if "Duplicate" in str(e):
            messagebox.showerror("Ошибка", f"Автомобиль с госномером '{license_plate}' уже существует!")
        else:
            messagebox.showerror("Ошибка", f"Не удалось обновить автомобиль:\n{e}")
        return False
    finally:
        cursor.close()
        connection.close()

def delete_car(car_id):
    """Удаление автомобиля по ID"""
    connection = get_db_connection()
    if not connection:
        return False
    cursor = connection.cursor()
    query = "DELETE FROM cars WHERE id = %s"
    try:
        cursor.execute(query, (car_id,))
        connection.commit()
        return True
    except Error as e:
        messagebox.showerror("Ошибка", f"Не удалось удалить автомобиль:\n{e}")
        return False
    finally:
        cursor.close()
        connection.close()

def get_owner_data(owner_id):
    """Получить полные данные владельца по ID"""
    if not owner_id:
        return None
    connection = get_db_connection()
    if not connection:
        return None
    cursor = connection.cursor()
    query = "SELECT full_name, phone, address FROM owners WHERE id = %s"
    try:
        cursor.execute(query, (owner_id,))
        result = cursor.fetchone()
        return result if result else None
    except Error as e:
        print(f"Ошибка получения данных владельца: {e}")
        return None
    finally:
        cursor.close()
        connection.close()



# ==================== ВАЛИДАЦИЯ ====================
def validate_fields(full_name, license_plate, brand, model):
    if not full_name.strip():
        messagebox.showwarning("Внимание", "Поле 'ФИО владельца' обязательно для заполнения!")
        return False
    if not license_plate.strip():
        messagebox.showwarning("Внимание", "Поле 'Госномер' обязательно для заполнения!")
        return False
    if not brand.strip():
        messagebox.showwarning("Внимание", "Поле 'Марка' обязательно для заполнения!")
        return False
    if not model.strip():
        messagebox.showwarning("Внимание", "Поле 'Модель' обязательно для заполнения!")
        return False
    return True

def validate_phone(phone):
    """Улучшенная проверка телефона (только цифры, +, пробелы, дефисы, скобки)"""
    if phone and not re.fullmatch(r'[\d\s\+\(\)\-]{10,20}', phone):
        messagebox.showwarning("Внимание", "Телефон должен содержать 10-20 символов (цифры, +, -, пробелы, скобки)")
        return False
    return True

def validate_year(year):
    if year:
        try:
            year_int = int(year)
            current_year = datetime.now().year
            if year_int < 1900 or year_int > current_year + 1:
                messagebox.showwarning("Внимание", f"Год выпуска должен быть в диапазоне 1900-{current_year+1}")
                return False
        except ValueError:
            messagebox.showwarning("Внимание", "Год выпуска должен быть числом!")
            return False
    return True

# ==================== ОКНО ДОБАВЛЕНИЯ/РЕДАКТИРОВАНИЯ ====================
class AddEditWindow:
    def __init__(self, parent, tree, edit_mode=False, car_id=None):
        self.parent = parent
        self.tree = tree
        self.edit_mode = edit_mode
        self.car_id = car_id          # ID автомобиля (если редактирование)

        self.window = Toplevel(parent)
        title = "Редактирование автомобиля" if edit_mode else "Добавление автомобиля"
        self.window.title(title)
        self.window.geometry("500x550")
        self.window.resizable(False, False)
        self.window.grab_set()

        self.create_widgets()

        if edit_mode and car_id:
            self.load_data_for_edit()

    def create_widgets(self):
        main_frame = ttk.Frame(self.window, padding="20")
        main_frame.pack(fill=tk.BOTH, expand=True)

        # Данные владельца
        ttk.Label(main_frame, text="ДАННЫЕ ВЛАДЕЛЬЦА", font=('Arial', 10, 'bold'))\
            .grid(row=0, column=0, columnspan=2, pady=(0,10), sticky='w')

        ttk.Label(main_frame, text="ФИО владельца:*").grid(row=1, column=0, pady=5, sticky='e')
        self.owner_name_entry = ttk.Entry(main_frame, width=30)
        self.owner_name_entry.grid(row=1, column=1, pady=5, sticky='w')

        ttk.Label(main_frame, text="Телефон:").grid(row=2, column=0, pady=5, sticky='e')
        self.phone_entry = ttk.Entry(main_frame, width=30)
        self.phone_entry.grid(row=2, column=1, pady=5, sticky='w')

        ttk.Label(main_frame, text="Адрес:").grid(row=3, column=0, pady=5, sticky='e')
        self.address_entry = ttk.Entry(main_frame, width=30)
        self.address_entry.grid(row=3, column=1, pady=5, sticky='w')

        ttk.Separator(main_frame, orient='horizontal').grid(row=4, column=0, columnspan=2, pady=15, sticky='ew')

        # Данные автомобиля
        ttk.Label(main_frame, text="ДАННЫЕ АВТОМОБИЛЯ", font=('Arial', 10, 'bold'))\
            .grid(row=5, column=0, columnspan=2, pady=(0,10), sticky='w')

        ttk.Label(main_frame, text="Госномер:*").grid(row=6, column=0, pady=5, sticky='e')
        self.plate_entry = ttk.Entry(main_frame, width=30)
        self.plate_entry.grid(row=6, column=1, pady=5, sticky='w')

        ttk.Label(main_frame, text="Марка:*").grid(row=7, column=0, pady=5, sticky='e')
        self.brand_entry = ttk.Entry(main_frame, width=30)
        self.brand_entry.grid(row=7, column=1, pady=5, sticky='w')



        ttk.Label(main_frame, text="Модель:*").grid(row=8, column=0, pady=5, sticky='e')
        self.model_entry = ttk.Entry(main_frame, width=30)
        self.model_entry.grid(row=8, column=1, pady=5, sticky='w')

        ttk.Label(main_frame, text="Год выпуска:").grid(row=9, column=0, pady=5, sticky='e')
        self.year_entry = ttk.Entry(main_frame, width=30)
        self.year_entry.grid(row=9, column=1, pady=5, sticky='w')

        ttk.Label(main_frame, text="Цвет:").grid(row=10, column=0, pady=5, sticky='e')
        self.color_entry = ttk.Entry(main_frame, width=30)
        self.color_entry.grid(row=10, column=1, pady=5, sticky='w')

        # Кнопки
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=11, column=0, columnspan=2, pady=20)

        save_btn = ttk.Button(button_frame, text="Сохранить", command=self.save_data)
        save_btn.pack(side=tk.LEFT, padx=5)

        cancel_btn = ttk.Button(button_frame, text="Отмена", command=self.window.destroy)
        cancel_btn.pack(side=tk.LEFT, padx=5)

        ttk.Label(main_frame, text="* - обязательные поля", font=('Arial', 8))\
            .grid(row=12, column=0, columnspan=2, pady=5)

    def load_data_for_edit(self):
        """Загрузка данных автомобиля и владельца для редактирования"""
        connection = get_db_connection()
        if not connection:
            return

        cursor = connection.cursor()
        # Получаем полные данные автомобиля
        query = """
            SELECT c.license_plate, c.brand, c.model, c.year, c.color,
                   c.owner_id, o.full_name, o.phone, o.address
            FROM cars c
            LEFT JOIN owners o ON c.owner_id = o.id
            WHERE c.id = %s
        """
        try:
            cursor.execute(query, (self.car_id,))
            row = cursor.fetchone()
            if row:
                self.plate_entry.insert(0, row[0] or '')
                self.brand_entry.insert(0, row[1] or '')
                self.model_entry.insert(0, row[2] or '')
                self.year_entry.insert(0, str(row[3]) if row[3] else '')
                self.color_entry.insert(0, row[4] or '')
                # Данные владельца
                self.owner_name_entry.insert(0, row[6] or '')
                self.phone_entry.insert(0, row[7] or '')
                self.address_entry.insert(0, row[8] or '')
        except Error as e:
            messagebox.showerror("Ошибка", f"Не удалось загрузить данные:\n{e}")
        finally:
            cursor.close()
            connection.close()

    def save_data(self):
        # Сбор данных
        owner_name = self.owner_name_entry.get().strip()
        phone = self.phone_entry.get().strip()
        address = self.address_entry.get().strip()
        license_plate = self.plate_entry.get().strip().upper()
        brand = self.brand_entry.get().strip()
        model = self.model_entry.get().strip()
        year_str = self.year_entry.get().strip()
        color = self.color_entry.get().strip()

        # Валидация
        if not validate_fields(owner_name, license_plate, brand, model):
            return
        if not validate_phone(phone):
            return
        if not validate_year(year_str):
            return

        year = int(year_str) if year_str else None

        if self.edit_mode:
            # Режим редактирования
            # Определяем owner_id (может быть NULL)
            owner_id = None
            # Если владелец указан, ищем или создаём
            if owner_name:
                owner_id = find_owner_by_name(owner_name)
                if not owner_id:
                    owner_id = add_owner(owner_name, phone, address)
                    if not owner_id:
                        return
                else:
                    # Обновляем данные существующего владельца
                    update_owner(owner_id, owner_name, phone, address)



            # Обновляем автомобиль
            if update_car(self.car_id, license_plate, brand, model, year, color, owner_id):
                messagebox.showinfo("Успех", "Данные успешно обновлены!")
                load_cars_data(self.tree)
                self.window.destroy()
        else:
            # Режим добавления
            owner_id = None
            if owner_name:
                owner_id = find_owner_by_name(owner_name)
                if not owner_id:
                    owner_id = add_owner(owner_name, phone, address)
                    if not owner_id:
                        return
                # Если владелец найден, НЕ обновляем его данные (оставляем как есть)
                # При желании изменить данные владельца нужно использовать редактирование

            if add_car(license_plate, brand, model, year, color, owner_id):
                messagebox.showinfo("Успех", "Автомобиль успешно добавлен!")
                load_cars_data(self.tree)
                self.window.destroy()

# ==================== ГЛАВНОЕ ОКНО ====================
class MainApplication:
    def __init__(self, root):
        self.root = root
        self.root.title("Автосервис - Учёт автомобилей")
        self.root.geometry("1200x500")
        self.root.resizable(True, True)

        self.create_widgets()
        self.load_data()

    def create_widgets(self):
        # Панель кнопок
        button_frame = ttk.Frame(self.root, padding="10")
        button_frame.pack(fill=tk.X)

        ttk.Button(button_frame, text="Добавить авто", command=self.open_add_window).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="Редактировать", command=self.open_edit_window).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="Удалить", command=self.delete_car).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="Обновить", command=self.load_data).pack(side=tk.LEFT, padx=5)

        # Таблица
        tree_frame = ttk.Frame(self.root, padding="10")
        tree_frame.pack(fill=tk.BOTH, expand=True)

        scroll_y = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL)
        scroll_x = ttk.Scrollbar(tree_frame, orient=tk.HORIZONTAL)

        columns = ('id', 'license_plate', 'brand', 'model', 'year', 'color', 'owner_name', 'phone', 'address')
        self.tree = ttk.Treeview(tree_frame, columns=columns, show='headings',
                                 yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)

        # Настройка заголовков
        self.tree.heading('id', text='ID')
        self.tree.heading('license_plate', text='Госномер')
        self.tree.heading('brand', text='Марка')
        self.tree.heading('model', text='Модель')
        self.tree.heading('year', text='Год')
        self.tree.heading('color', text='Цвет')
        self.tree.heading('owner_name', text='Владелец (ФИО)')
        self.tree.heading('phone', text='Телефон')
        self.tree.heading('address', text='Адрес')

        # Ширина колонок
        self.tree.column('id', width=40, anchor='center')
        self.tree.column('license_plate', width=100)
        self.tree.column('brand', width=100)
        self.tree.column('model', width=100)
        self.tree.column('year', width=60, anchor='center')
        self.tree.column('color', width=80)
        self.tree.column('owner_name', width=200)
        self.tree.column('phone', width=120)
        self.tree.column('address', width=200)

        scroll_y.config(command=self.tree.yview)
        scroll_x.config(command=self.tree.xview)

        self.tree.grid(row=0, column=0, sticky='nsew')
        scroll_y.grid(row=0, column=1, sticky='ns')
        scroll_x.grid(row=1, column=0, sticky='ew')

        tree_frame.grid_rowconfigure(0, weight=1)
        tree_frame.grid_columnconfigure(0, weight=1)

    def load_data(self):
        load_cars_data(self.tree)

    def open_add_window(self):
        AddEditWindow(self.root, self.tree, edit_mode=False)



    def open_edit_window(self):
        selected = self.tree.selection()
        if not selected:
            messagebox.showwarning("Предупреждение", "Выберите запись для редактирования")
            return
        car_id = int(selected[0])   # ID хранится как iid
        AddEditWindow(self.root, self.tree, edit_mode=True, car_id=car_id)

    def delete_car(self):
        selected = self.tree.selection()
        if not selected:
            messagebox.showwarning("Предупреждение", "Выберите запись для удаления")
            return

        car_id = int(selected[0])
        values = self.tree.item(selected[0])['values']
        license_plate = values[1]   # госномер
        brand = values[2]
        model = values[3]

        result = messagebox.askyesno(
            "Подтверждение удаления",
            f"Вы уверены, что хотите удалить автомобиль {license_plate} ({brand} {model})?\n"
            "Владелец останется в базе, если у него есть другие автомобили."
        )
        if result:
            if delete_car(car_id):
                messagebox.showinfo("Успех", "Автомобиль успешно удалён!")
                self.load_data()
            else:
                messagebox.showerror("Ошибка", "Не удалось удалить автомобиль!")

# ==================== ТОЧКА ВХОДА ====================
if __name__ == "__main__":
    if init_db():
        root = tk.Tk()
        app = MainApplication(root)
        root.mainloop()
    else:
        print("Ошибка инициализации базы данных. Приложение закрыто.")