diff --git a/control1-2.py b/control1-2.py new file mode 100644 index 0000000..360f50d --- /dev/null +++ b/control1-2.py @@ -0,0 +1,1302 @@ +import sys +import sqlite3 +from datetime import datetime +from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QTextEdit, QComboBox, QPushButton, QTableWidget, + QTableWidgetItem, QTabWidget, QGroupBox, QMessageBox, QFileDialog, + QSplitter, QHeaderView, QFormLayout, QCheckBox, QDialog, QDateEdit) +from PyQt6.QtCore import Qt, pyqtSignal, QDate +from PyQt6.QtGui import QIcon, QAction +import os + +class DatabaseManager: + def __init__(self): + self.conn = sqlite3.connect('service_requests_v2.db') + self.create_tables() + + def create_tables(self): + cursor = self.conn.cursor() + + # Таблица пользователей + cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + role TEXT NOT NULL, + full_name TEXT NOT NULL, + phone TEXT, + email TEXT + ) + ''') + + # Таблица заявок с расширенными полями + cursor.execute(''' + CREATE TABLE IF NOT EXISTS service_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + client_name TEXT NOT NULL, + client_phone TEXT NOT NULL, + client_email TEXT, + equipment_type TEXT NOT NULL, + equipment_model TEXT NOT NULL, + serial_number TEXT, + problem_description TEXT NOT NULL, + status TEXT DEFAULT 'Новая', + priority TEXT DEFAULT 'Средний', + request_type TEXT DEFAULT 'Ремонт', + assigned_operator TEXT, + assigned_master TEXT, + observer_group TEXT, + created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + completed_date TIMESTAMP, + deadline DATE, + marked_for_deletion BOOLEAN DEFAULT 0, + duplicate_of INTEGER + ) + ''') + + # Таблица истории заявок + cursor.execute(''' + CREATE TABLE IF NOT EXISTS request_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_id INTEGER, + user_id INTEGER, + action TEXT, + details TEXT, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (request_id) REFERENCES service_requests (id) + ) + ''') + + # Таблица вложений + cursor.execute(''' + CREATE TABLE IF NOT EXISTS attachments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_id INTEGER, + filename TEXT, + filepath TEXT, + file_type TEXT, + uploaded_by INTEGER, + upload_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (request_id) REFERENCES service_requests (id) + ) + ''') + + # Таблица отчетов + cursor.execute(''' + CREATE TABLE IF NOT EXISTS reports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_id INTEGER, + master_id INTEGER, + work_description TEXT, + parts_used TEXT, + time_spent REAL, + labor_cost REAL, + parts_cost REAL, + total_cost REAL, + created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (request_id) REFERENCES service_requests (id) + ) + ''') + + # Таблица заказов запчастей + cursor.execute(''' + CREATE TABLE IF NOT EXISTS part_orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_id INTEGER, + part_name TEXT, + part_number TEXT, + quantity INTEGER, + supplier TEXT, + estimated_cost REAL, + status TEXT DEFAULT 'Заказан', + ordered_by INTEGER, + order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + expected_date DATE, + FOREIGN KEY (request_id) REFERENCES service_requests (id) + ) + ''') + + # Таблица МТР (материально-технических ресурсов) + cursor.execute(''' + CREATE TABLE IF NOT EXISTS material_needs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_id INTEGER, + material_name TEXT, + material_type TEXT, + quantity INTEGER, + unit TEXT, + urgency TEXT DEFAULT 'Обычная', + status TEXT DEFAULT 'Требуется', + estimated_cost REAL, + notes TEXT, + created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (request_id) REFERENCES service_requests (id) + ) + ''') + + # Создаем тестовых пользователей для варианта 2 + self.create_test_users() + + self.conn.commit() + + def create_test_users(self): + cursor = self.conn.cursor() + + test_users = [ + ('operator1', '123', 'operator', 'Петр Петров', '+79167654321', 'operator@company.ru'), + ('master1', '123', 'master', 'Сергей Сергеев', '+79169998877', 'master@company.ru'), + ('admin1', '123', 'admin', 'Администратор Системы', '+79160001122', 'admin@company.ru') + ] + + for user in test_users: + try: + cursor.execute( + 'INSERT INTO users (username, password, role, full_name, phone, email) VALUES (?, ?, ?, ?, ?, ?)', + user + ) + except sqlite3.IntegrityError: + pass # Пользователь уже существует + + self.conn.commit() + +class ServiceRequestAppV2(QMainWindow): + def __init__(self): + super().__init__() + self.db = DatabaseManager() + self.current_user = None + self.init_ui() + + def init_ui(self): + self.setWindowTitle('Система учета заявок на ремонт оргтехники - Вариант 2') + self.setGeometry(100, 100, 1400, 800) + + # Центральный виджет + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # Основной layout + layout = QVBoxLayout(central_widget) + + # Панель входа + self.login_widget = self.create_login_widget() + layout.addWidget(self.login_widget) + + # Основной интерфейс (скрыт до входа) + self.main_tabs = QTabWidget() + self.main_tabs.setVisible(False) + layout.addWidget(self.main_tabs) + + def create_login_widget(self): + widget = QWidget() + layout = QVBoxLayout(widget) + + layout.addWidget(QLabel('Вход в систему - Вариант 2')) + + form_layout = QFormLayout() + + self.username_input = QLineEdit() + self.password_input = QLineEdit() + self.password_input.setEchoMode(QLineEdit.EchoMode.Password) + + form_layout.addRow('Логин:', self.username_input) + form_layout.addRow('Пароль:', self.password_input) + + layout.addLayout(form_layout) + + login_btn = QPushButton('Войти') + login_btn.clicked.connect(self.login) + layout.addWidget(login_btn) + + # Подсказка с тестовыми пользователями + hint = QLabel('Тестовые пользователи: operator1/123, master1/123, admin1/123') + hint.setStyleSheet('color: gray; font-size: 10px;') + layout.addWidget(hint) + + return widget + + def login(self): + username = self.username_input.text() + password = self.password_input.text() + + cursor = self.db.conn.cursor() + cursor.execute( + 'SELECT * FROM users WHERE username = ? AND password = ?', + (username, password) + ) + + user = cursor.fetchone() + + if user: + self.current_user = { + 'id': user[0], + 'username': user[1], + 'role': user[3], + 'full_name': user[4] + } + self.show_main_interface() + else: + QMessageBox.warning(self, 'Ошибка', 'Неверный логин или пароль') + + def show_main_interface(self): + self.login_widget.setVisible(False) + self.main_tabs.setVisible(True) + + # Очищаем предыдущие вкладки + self.main_tabs.clear() + + role = self.current_user['role'] + + if role == 'operator': + self.setup_operator_interface() + elif role == 'master': + self.setup_master_interface() + elif role == 'admin': + self.setup_admin_interface() + + def setup_operator_interface(self): + # Вкладка регистрации заявок + register_tab = QWidget() + layout = QVBoxLayout(register_tab) + + layout.addWidget(QLabel('Регистрация новой заявки')) + + form_layout = QFormLayout() + + self.op_client_name = QLineEdit() + self.op_client_phone = QLineEdit() + self.op_client_email = QLineEdit() + self.op_equipment_type = QComboBox() + self.op_equipment_type.addItems(['Принтер', 'Копир', 'Сканер', 'МФУ', 'Компьютер', 'Монитор', 'Телефон', 'Другое']) + self.op_equipment_model = QLineEdit() + self.op_serial_number = QLineEdit() + self.op_problem_description = QTextEdit() + + form_layout.addRow('ФИО клиента:', self.op_client_name) + form_layout.addRow('Телефон:', self.op_client_phone) + form_layout.addRow('Email:', self.op_client_email) + form_layout.addRow('Тип оборудования:', self.op_equipment_type) + form_layout.addRow('Модель:', self.op_equipment_model) + form_layout.addRow('Серийный номер:', self.op_serial_number) + form_layout.addRow('Описание проблемы:', self.op_problem_description) + + layout.addLayout(form_layout) + + submit_btn = QPushButton('Зарегистрировать заявку') + submit_btn.clicked.connect(self.register_service_request) + layout.addWidget(submit_btn) + + self.main_tabs.addTab(register_tab, 'Регистрация заявки') + + # Вкладка управления заявками + management_tab = QWidget() + layout = QVBoxLayout(management_tab) + + # Панель фильтров + filter_layout = QHBoxLayout() + filter_layout.addWidget(QLabel('Статус:')) + self.status_filter = QComboBox() + self.status_filter.addItems(['Все', 'Новая', 'В работе', 'Ожидает запчасти', 'Выполнена', 'Отменена']) + filter_layout.addWidget(self.status_filter) + + filter_layout.addWidget(QLabel('Приоритет:')) + self.priority_filter = QComboBox() + self.priority_filter.addItems(['Все', 'Низкий', 'Средний', 'Высокий', 'Критичный']) + filter_layout.addWidget(self.priority_filter) + + filter_btn = QPushButton('Применить фильтры') + filter_btn.clicked.connect(self.load_operator_requests) + filter_layout.addWidget(filter_btn) + + layout.addLayout(filter_layout) + + self.operator_requests_table = QTableWidget() + self.operator_requests_table.setColumnCount(10) + self.operator_requests_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Оборудование', 'Модель', 'Статус', 'Приоритет', 'Тип', 'Дата', 'Оператор', 'Помечена на удаление' + ]) + layout.addWidget(self.operator_requests_table) + + # Панель управления заявкой + control_group = QGroupBox('Управление заявкой') + control_layout = QHBoxLayout(control_group) + + self.status_combo = QComboBox() + self.status_combo.addItems(['Новая', 'В работе', 'Ожидает запчасти', 'Выполнена', 'Отменена']) + + self.priority_combo = QComboBox() + self.priority_combo.addItems(['Низкий', 'Средний', 'Высокий', 'Критичный']) + + self.type_combo = QComboBox() + self.type_combo.addItems(['Ремонт', 'Обслуживание', 'Консультация', 'Диагностика']) + + self.observer_group = QLineEdit() + self.observer_group.setPlaceholderText('Группа наблюдателей') + + mark_delete_btn = QPushButton('Пометить на удаление') + mark_delete_btn.clicked.connect(self.mark_request_for_deletion) + + update_btn = QPushButton('Обновить заявку') + update_btn.clicked.connect(self.update_request_operator) + + control_layout.addWidget(QLabel('Статус:')) + control_layout.addWidget(self.status_combo) + control_layout.addWidget(QLabel('Приоритет:')) + control_layout.addWidget(self.priority_combo) + control_layout.addWidget(QLabel('Тип:')) + control_layout.addWidget(self.type_combo) + control_layout.addWidget(self.observer_group) + control_layout.addWidget(update_btn) + control_layout.addWidget(mark_delete_btn) + + layout.addWidget(control_group) + + self.main_tabs.addTab(management_tab, 'Управление заявками') + + # Вкладка архива + archive_tab = QWidget() + layout = QVBoxLayout(archive_tab) + + search_layout = QHBoxLayout() + self.archive_search = QLineEdit() + self.archive_search.setPlaceholderText('Поиск в архиве...') + search_btn = QPushButton('Найти') + search_btn.clicked.connect(self.search_archive) + + search_layout.addWidget(self.archive_search) + search_layout.addWidget(search_btn) + layout.addLayout(search_layout) + + self.archive_table = QTableWidget() + self.archive_table.setColumnCount(9) + self.archive_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Оборудование', 'Модель', 'Статус', 'Дата создания', 'Дата завершения', 'Мастер', 'Тип' + ]) + layout.addWidget(self.archive_table) + + self.main_tabs.addTab(archive_tab, 'Архив') + + self.load_operator_requests() + self.load_archive() + + def setup_master_interface(self): + # Вкладка назначенных заявок + requests_tab = QWidget() + layout = QVBoxLayout(requests_tab) + + self.master_requests_table = QTableWidget() + self.master_requests_table.setColumnCount(8) + self.master_requests_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Оборудование', 'Модель', 'Статус', 'Приоритет', 'Дата', 'Срок' + ]) + layout.addWidget(self.master_requests_table) + + # Панель управления для мастера + control_group = QGroupBox('Управление ремонтом') + control_layout = QVBoxLayout(control_group) + + # Смена статуса + status_layout = QHBoxLayout() + status_layout.addWidget(QLabel('Статус:')) + self.master_status_combo = QComboBox() + self.master_status_combo.addItems(['В работе', 'Ожидает запчасти', 'Выполнена', 'Отменена']) + status_layout.addWidget(self.master_status_combo) + + update_status_btn = QPushButton('Обновить статус') + update_status_btn.clicked.connect(self.update_request_master) + status_layout.addWidget(update_status_btn) + + control_layout.addLayout(status_layout) + + # Заказ запчастей + parts_group = QGroupBox('Заказ запчастей') + parts_layout = QFormLayout(parts_group) + + self.part_name = QLineEdit() + self.part_number = QLineEdit() + self.part_quantity = QLineEdit() + self.part_quantity.setText('1') + self.part_supplier = QLineEdit() + self.part_cost = QLineEdit() + + parts_layout.addRow('Название запчасти:', self.part_name) + parts_layout.addRow('Номер запчасти:', self.part_number) + parts_layout.addRow('Количество:', self.part_quantity) + parts_layout.addRow('Поставщик:', self.part_supplier) + parts_layout.addRow('Примерная стоимость:', self.part_cost) + + order_parts_btn = QPushButton('Заказать запчасти') + order_parts_btn.clicked.connect(self.order_parts) + parts_layout.addRow(order_parts_btn) + + control_layout.addWidget(parts_group) + + # Прикрепление файлов + file_layout = QHBoxLayout() + attach_photo_btn = QPushButton('Прикрепить фото') + attach_photo_btn.clicked.connect(self.attach_photo) + attach_file_btn = QPushButton('Прикрепить файл') + attach_file_btn.clicked.connect(self.attach_file) + + file_layout.addWidget(attach_photo_btn) + file_layout.addWidget(attach_file_btn) + control_layout.addLayout(file_layout) + + # Создание отчета + report_btn = QPushButton('Создать отчет о выполненной работе') + report_btn.clicked.connect(self.create_report) + control_layout.addWidget(report_btn) + + # История заявки + history_btn = QPushButton('Просмотреть историю заявки') + history_btn.clicked.connect(self.show_request_history) + control_layout.addWidget(history_btn) + + layout.addWidget(control_group) + + self.main_tabs.addTab(requests_tab, 'Мои заявки') + + # Вкладка заказанных запчастей + parts_tab = QWidget() + layout = QVBoxLayout(parts_tab) + + self.parts_table = QTableWidget() + self.parts_table.setColumnCount(8) + self.parts_table.setHorizontalHeaderLabels([ + 'ID', 'Заявка', 'Запчасть', 'Номер', 'Кол-во', 'Статус', 'Дата заказа', 'Поставщик' + ]) + layout.addWidget(self.parts_table) + + self.main_tabs.addTab(parts_tab, 'Заказанные запчасти') + + self.load_master_requests() + self.load_master_parts() + + def setup_admin_interface(self): + # Вкладка управления пользователями + users_tab = QWidget() + layout = QVBoxLayout(users_tab) + + self.users_table = QTableWidget() + self.users_table.setColumnCount(6) + self.users_table.setHorizontalHeaderLabels(['ID', 'Логин', 'ФИО', 'Роль', 'Телефон', 'Email']) + layout.addWidget(self.users_table) + + add_user_btn = QPushButton('Добавить пользователя') + add_user_btn.clicked.connect(self.show_add_user_dialog) + layout.addWidget(add_user_btn) + + self.main_tabs.addTab(users_tab, 'Пользователи') + + # Вкладка всех заявок + requests_tab = QWidget() + layout = QVBoxLayout(requests_tab) + + self.admin_requests_table = QTableWidget() + self.admin_requests_table.setColumnCount(11) + self.admin_requests_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Оборудование', 'Модель', 'Статус', 'Приоритет', 'Тип', 'Дата', 'Оператор', 'Мастер', 'Помечена на удаление' + ]) + layout.addWidget(self.admin_requests_table) + + delete_btn = QPushButton('Удалить выбранные заявки') + delete_btn.clicked.connect(self.delete_marked_requests) + layout.addWidget(delete_btn) + + self.main_tabs.addTab(requests_tab, 'Все заявки') + + # Вкладка распределения заявок + distribution_tab = QWidget() + layout = QVBoxLayout(distribution_tab) + + self.distribution_table = QTableWidget() + self.distribution_table.setColumnCount(9) + self.distribution_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Оборудование', 'Статус', 'Оператор', 'Мастер', 'Группа наблюдателей', 'Срок исполнения', 'Приоритет' + ]) + layout.addWidget(self.distribution_table) + + self.main_tabs.addTab(distribution_tab, 'Распределение заявок') + + # Вкладка МТР (материально-технических ресурсов) + materials_tab = QWidget() + layout = QVBoxLayout(materials_tab) + + # Панель управления МТР + mtr_control_layout = QHBoxLayout() + consolidate_btn = QPushButton('Консолидировать потребности в МТР') + consolidate_btn.clicked.connect(self.consolidate_material_needs) + generate_report_btn = QPushButton('Сформировать отчет по МТР') + generate_report_btn.clicked.connect(self.generate_mtr_report) + + mtr_control_layout.addWidget(consolidate_btn) + mtr_control_layout.addWidget(generate_report_btn) + layout.addLayout(mtr_control_layout) + + self.materials_table = QTableWidget() + self.materials_table.setColumnCount(10) + self.materials_table.setHorizontalHeaderLabels([ + 'ID', 'Заявка', 'Материал', 'Тип', 'Кол-во', 'Ед.изм', 'Срочность', 'Статус', 'Примерная стоимость', 'Примечания' + ]) + layout.addWidget(self.materials_table) + + self.main_tabs.addTab(materials_tab, 'МТР') + + # Вкладка архива + archive_tab = QWidget() + layout = QVBoxLayout(archive_tab) + + search_layout = QHBoxLayout() + self.admin_archive_search = QLineEdit() + self.admin_archive_search.setPlaceholderText('Поиск в архиве...') + admin_search_btn = QPushButton('Найти') + admin_search_btn.clicked.connect(self.search_admin_archive) + + search_layout.addWidget(self.admin_archive_search) + search_layout.addWidget(admin_search_btn) + layout.addLayout(search_layout) + + self.admin_archive_table = QTableWidget() + self.admin_archive_table.setColumnCount(10) + self.admin_archive_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Оборудование', 'Модель', 'Статус', 'Дата создания', 'Дата завершения', 'Мастер', 'Тип', 'Стоимость' + ]) + layout.addWidget(self.admin_archive_table) + + self.main_tabs.addTab(archive_tab, 'Архив') + + self.load_users() + self.load_admin_requests() + self.load_distribution() + self.load_materials() + self.load_admin_archive() + + def register_service_request(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + INSERT INTO service_requests + (client_name, client_phone, client_email, equipment_type, equipment_model, serial_number, problem_description, assigned_operator) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + self.op_client_name.text(), + self.op_client_phone.text(), + self.op_client_email.text(), + self.op_equipment_type.currentText(), + self.op_equipment_model.text(), + self.op_serial_number.text(), + self.op_problem_description.toPlainText(), + self.current_user['full_name'] + )) + + self.db.conn.commit() + + # Запись в историю + request_id = cursor.lastrowid + cursor.execute(''' + INSERT INTO request_history (request_id, user_id, action, details) + VALUES (?, ?, ?, ?) + ''', (request_id, self.current_user['id'], 'Создание заявки', 'Заявка зарегистрирована оператором')) + + self.db.conn.commit() + + QMessageBox.information(self, 'Успех', 'Заявка успешно зарегистрирована!') + + # Очищаем форму + self.op_client_name.clear() + self.op_client_phone.clear() + self.op_client_email.clear() + self.op_equipment_model.clear() + self.op_serial_number.clear() + self.op_problem_description.clear() + + def load_operator_requests(self): + cursor = self.db.conn.cursor() + + status_filter = self.status_filter.currentText() + priority_filter = self.priority_filter.currentText() + + query = ''' + SELECT id, client_name, equipment_type, equipment_model, status, priority, request_type, + created_date, assigned_operator, marked_for_deletion + FROM service_requests + WHERE 1=1 + ''' + params = [] + + if status_filter != 'Все': + query += ' AND status = ?' + params.append(status_filter) + + if priority_filter != 'Все': + query += ' AND priority = ?' + params.append(priority_filter) + + query += ' ORDER BY created_date DESC' + + cursor.execute(query, params) + requests = cursor.fetchall() + + self.operator_requests_table.setRowCount(len(requests)) + for row, request in enumerate(requests): + for col, value in enumerate(request): + item = QTableWidgetItem(str(value) if value is not None else '') + + # Помечаем заявки на удаление + if col == 9 and value == 1: + item.setBackground(Qt.GlobalColor.yellow) + + self.operator_requests_table.setItem(row, col, item) + + def load_master_requests(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT id, client_name, equipment_type, equipment_model, status, priority, created_date, deadline + FROM service_requests + WHERE status != 'Выполнена' AND status != 'Отменена' + ORDER BY + CASE priority + WHEN 'Критичный' THEN 1 + WHEN 'Высокий' THEN 2 + WHEN 'Средний' THEN 3 + WHEN 'Низкий' THEN 4 + END, + created_date + ''') + + requests = cursor.fetchall() + + self.master_requests_table.setRowCount(len(requests)) + for row, request in enumerate(requests): + for col, value in enumerate(request): + item = QTableWidgetItem(str(value) if value is not None else '') + + # Подсвечиваем просроченные заявки + if col == 7 and value: + deadline = QDate.fromString(value, 'yyyy-MM-dd') + if deadline < QDate.currentDate(): + item.setBackground(Qt.GlobalColor.red) + + self.master_requests_table.setItem(row, col, item) + + def load_master_parts(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT po.id, sr.id, po.part_name, po.part_number, po.quantity, po.status, po.order_date, po.supplier + FROM part_orders po + JOIN service_requests sr ON po.request_id = sr.id + WHERE sr.assigned_master = ? OR ? = 'admin1' + ORDER BY po.order_date DESC + ''', (self.current_user['full_name'], self.current_user['username'])) + + parts = cursor.fetchall() + + self.parts_table.setRowCount(len(parts)) + for row, part in enumerate(parts): + for col, value in enumerate(part): + self.parts_table.setItem(row, col, QTableWidgetItem(str(value) if value is not None else '')) + + def load_admin_requests(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT id, client_name, equipment_type, equipment_model, status, priority, request_type, + created_date, assigned_operator, assigned_master, marked_for_deletion + FROM service_requests + ORDER BY created_date DESC + ''') + + requests = cursor.fetchall() + + self.admin_requests_table.setRowCount(len(requests)) + for row, request in enumerate(requests): + for col, value in enumerate(request): + item = QTableWidgetItem(str(value) if value is not None else '') + + # Помечаем заявки на удаление + if col == 10 and value == 1: + item.setBackground(Qt.GlobalColor.yellow) + + self.admin_requests_table.setItem(row, col, item) + + def load_distribution(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT id, client_name, equipment_type, status, assigned_operator, assigned_master, + observer_group, deadline, priority + FROM service_requests + WHERE status != 'Выполнена' AND status != 'Отменена' + ORDER BY priority, created_date + ''') + + distributions = cursor.fetchall() + + self.distribution_table.setRowCount(len(distributions)) + for row, dist in enumerate(distributions): + for col, value in enumerate(dist): + item = QTableWidgetItem(str(value) if value is not None else '') + self.distribution_table.setItem(row, col, item) + + def load_materials(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT mn.id, sr.id, mn.material_name, mn.material_type, mn.quantity, mn.unit, + mn.urgency, mn.status, mn.estimated_cost, mn.notes + FROM material_needs mn + JOIN service_requests sr ON mn.request_id = sr.id + ORDER BY mn.urgency DESC, mn.created_date + ''') + + materials = cursor.fetchall() + + self.materials_table.setRowCount(len(materials)) + for row, material in enumerate(materials): + for col, value in enumerate(material): + self.materials_table.setItem(row, col, QTableWidgetItem(str(value) if value is not None else '')) + + def load_archive(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT id, client_name, equipment_type, equipment_model, status, created_date, completed_date, assigned_master, request_type + FROM service_requests + WHERE status = 'Выполнена' + ORDER BY completed_date DESC + ''') + + requests = cursor.fetchall() + + self.archive_table.setRowCount(len(requests)) + for row, request in enumerate(requests): + for col, value in enumerate(request): + self.archive_table.setItem(row, col, QTableWidgetItem(str(value) if value is not None else '')) + + def load_admin_archive(self): + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT sr.id, sr.client_name, sr.equipment_type, sr.equipment_model, sr.status, + sr.created_date, sr.completed_date, sr.assigned_master, sr.request_type, + COALESCE(r.total_cost, 0) + FROM service_requests sr + LEFT JOIN reports r ON sr.id = r.request_id + WHERE sr.status = 'Выполнена' + ORDER BY sr.completed_date DESC + ''') + + requests = cursor.fetchall() + + self.admin_archive_table.setRowCount(len(requests)) + for row, request in enumerate(requests): + for col, value in enumerate(request): + self.admin_archive_table.setItem(row, col, QTableWidgetItem(str(value) if value is not None else '')) + + def load_users(self): + cursor = self.db.conn.cursor() + cursor.execute('SELECT id, username, full_name, role, phone, email FROM users') + + users = cursor.fetchall() + + self.users_table.setRowCount(len(users)) + for row, user in enumerate(users): + for col, value in enumerate(user): + self.users_table.setItem(row, col, QTableWidgetItem(str(value))) + + def update_request_operator(self): + current_row = self.operator_requests_table.currentRow() + if current_row >= 0: + request_id = self.operator_requests_table.item(current_row, 0).text() + + cursor = self.db.conn.cursor() + cursor.execute(''' + UPDATE service_requests + SET status = ?, priority = ?, request_type = ?, observer_group = ?, assigned_operator = ? + WHERE id = ? + ''', ( + self.status_combo.currentText(), + self.priority_combo.currentText(), + self.type_combo.currentText(), + self.observer_group.text(), + self.current_user['full_name'], + request_id + )) + + # Запись в историю + cursor.execute(''' + INSERT INTO request_history (request_id, user_id, action, details) + VALUES (?, ?, ?, ?) + ''', (request_id, self.current_user['id'], 'Изменение заявки', + f'Оператор изменил статус на {self.status_combo.currentText()}, приоритет на {self.priority_combo.currentText()}')) + + self.db.conn.commit() + self.load_operator_requests() + QMessageBox.information(self, 'Успех', 'Заявка обновлена!') + + def mark_request_for_deletion(self): + current_row = self.operator_requests_table.currentRow() + if current_row >= 0: + request_id = self.operator_requests_table.item(current_row, 0).text() + + cursor = self.db.conn.cursor() + cursor.execute(''' + UPDATE service_requests + SET marked_for_deletion = 1 + WHERE id = ? + ''', (request_id,)) + + self.db.conn.commit() + self.load_operator_requests() + QMessageBox.information(self, 'Успех', 'Заявка помечена на удаление!') + + def update_request_master(self): + current_row = self.master_requests_table.currentRow() + if current_row >= 0: + request_id = self.master_requests_table.item(current_row, 0).text() + + cursor = self.db.conn.cursor() + cursor.execute(''' + UPDATE service_requests + SET status = ?, assigned_master = ? + WHERE id = ? + ''', ( + self.master_status_combo.currentText(), + self.current_user['full_name'], + request_id + )) + + # Запись в историю + cursor.execute(''' + INSERT INTO request_history (request_id, user_id, action, details) + VALUES (?, ?, ?, ?) + ''', (request_id, self.current_user['id'], 'Изменение статуса', + f'Мастер изменил статус на {self.master_status_combo.currentText()}')) + + self.db.conn.commit() + self.load_master_requests() + QMessageBox.information(self, 'Успех', 'Статус заявки обновлен!') + + def order_parts(self): + current_row = self.master_requests_table.currentRow() + if current_row >= 0: + request_id = self.master_requests_table.item(current_row, 0).text() + + cursor = self.db.conn.cursor() + cursor.execute(''' + INSERT INTO part_orders (request_id, part_name, part_number, quantity, supplier, estimated_cost, ordered_by) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', ( + request_id, + self.part_name.text(), + self.part_number.text(), + int(self.part_quantity.text()), + self.part_supplier.text(), + float(self.part_cost.text() or 0), + self.current_user['id'] + )) + + self.db.conn.commit() + + # Обновляем статус заявки + cursor.execute(''' + UPDATE service_requests + SET status = 'Ожидает запчасти' + WHERE id = ? + ''', (request_id,)) + + # Запись в историю + cursor.execute(''' + INSERT INTO request_history (request_id, user_id, action, details) + VALUES (?, ?, ?, ?) + ''', (request_id, self.current_user['id'], 'Заказ запчастей', + f'Заказана запчасть: {self.part_name.text()}, количество: {self.part_quantity.text()}')) + + self.db.conn.commit() + + # Очищаем форму + self.part_name.clear() + self.part_number.clear() + self.part_quantity.setText('1') + self.part_supplier.clear() + self.part_cost.clear() + + self.load_master_requests() + self.load_master_parts() + + QMessageBox.information(self, 'Успех', 'Запчасти заказаны!') + + def attach_photo(self): + self.attach_file(file_type='photo') + + def attach_file(self, file_type='document'): + current_row = self.master_requests_table.currentRow() + if current_row >= 0: + request_id = self.master_requests_table.item(current_row, 0).text() + + file_path, _ = QFileDialog.getOpenFileName(self, 'Выберите файл') + if file_path: + filename = os.path.basename(file_path) + + cursor = self.db.conn.cursor() + cursor.execute(''' + INSERT INTO attachments (request_id, filename, filepath, file_type, uploaded_by) + VALUES (?, ?, ?, ?, ?) + ''', ( + request_id, + filename, + file_path, + file_type, + self.current_user['id'] + )) + + # Запись в историю + cursor.execute(''' + INSERT INTO request_history (request_id, user_id, action, details) + VALUES (?, ?, ?, ?) + ''', (request_id, self.current_user['id'], 'Прикрепление файла', + f'Прикреплен файл: {filename}')) + + self.db.conn.commit() + QMessageBox.information(self, 'Успех', 'Файл прикреплен!') + + def create_report(self): + current_row = self.master_requests_table.currentRow() + if current_row >= 0: + request_id = self.master_requests_table.item(current_row, 0).text() + + # Диалог для ввода отчета + report_dialog = QDialog(self) + report_dialog.setWindowTitle('Создание отчета о выполненной работе') + report_dialog.setModal(True) + report_dialog.resize(500, 400) + layout = QVBoxLayout(report_dialog) + + form_layout = QFormLayout() + work_description = QTextEdit() + parts_used = QLineEdit() + time_spent = QLineEdit() + labor_cost = QLineEdit() + parts_cost = QLineEdit() + + form_layout.addRow('Описание выполненной работы:', work_description) + form_layout.addRow('Использованные запчасти:', parts_used) + form_layout.addRow('Затраченное время (часы):', time_spent) + form_layout.addRow('Стоимость работы:', labor_cost) + form_layout.addRow('Стоимость запчастей:', parts_cost) + + layout.addLayout(form_layout) + + buttons_layout = QHBoxLayout() + save_btn = QPushButton('Сохранить отчет') + cancel_btn = QPushButton('Отмена') + + buttons_layout.addWidget(save_btn) + buttons_layout.addWidget(cancel_btn) + layout.addLayout(buttons_layout) + + def save_report(): + total = float(labor_cost.text() or 0) + float(parts_cost.text() or 0) + + cursor = self.db.conn.cursor() + cursor.execute(''' + INSERT INTO reports (request_id, master_id, work_description, parts_used, time_spent, labor_cost, parts_cost, total_cost) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + request_id, + self.current_user['id'], + work_description.toPlainText(), + parts_used.text(), + float(time_spent.text() or 0), + float(labor_cost.text() or 0), + float(parts_cost.text() or 0), + total + )) + + # Обновляем статус заявки на выполненную + cursor.execute(''' + UPDATE service_requests + SET status = 'Выполнена', completed_date = CURRENT_TIMESTAMP + WHERE id = ? + ''', (request_id,)) + + # Запись в историю + cursor.execute(''' + INSERT INTO request_history (request_id, user_id, action, details) + VALUES (?, ?, ?, ?) + ''', (request_id, self.current_user['id'], 'Создание отчета', + 'Отчет о выполненной работе создан')) + + self.db.conn.commit() + report_dialog.accept() + self.load_master_requests() + QMessageBox.information(self, 'Успех', 'Отчет создан!') + + save_btn.clicked.connect(save_report) + cancel_btn.clicked.connect(report_dialog.reject) + + report_dialog.exec() + + def show_request_history(self): + current_row = self.master_requests_table.currentRow() + if current_row >= 0: + request_id = self.master_requests_table.item(current_row, 0).text() + + cursor = self.db.conn.cursor() + cursor.execute(''' + SELECT h.timestamp, u.full_name, h.action, h.details + FROM request_history h + JOIN users u ON h.user_id = u.id + WHERE h.request_id = ? + ORDER BY h.timestamp DESC + ''', (request_id,)) + + history = cursor.fetchall() + + history_dialog = QDialog(self) + history_dialog.setWindowTitle(f'История заявки #{request_id}') + history_dialog.setModal(True) + history_dialog.resize(600, 400) + layout = QVBoxLayout(history_dialog) + + history_table = QTableWidget() + history_table.setColumnCount(4) + history_table.setHorizontalHeaderLabels(['Дата', 'Пользователь', 'Действие', 'Детали']) + history_table.setRowCount(len(history)) + + for row, record in enumerate(history): + for col, value in enumerate(record): + history_table.setItem(row, col, QTableWidgetItem(str(value))) + + layout.addWidget(history_table) + + close_btn = QPushButton('Закрыть') + close_btn.clicked.connect(history_dialog.accept) + layout.addWidget(close_btn) + + history_dialog.exec() + + def search_archive(self): + search_text = self.archive_search.text() + cursor = self.db.conn.cursor() + + if search_text: + cursor.execute(''' + SELECT id, client_name, equipment_type, equipment_model, status, created_date, completed_date, assigned_master, request_type + FROM service_requests + WHERE status = 'Выполнена' AND + (client_name LIKE ? OR equipment_type LIKE ? OR equipment_model LIKE ? OR assigned_master LIKE ?) + ORDER BY completed_date DESC + ''', (f'%{search_text}%', f'%{search_text}%', f'%{search_text}%', f'%{search_text}%')) + else: + cursor.execute(''' + SELECT id, client_name, equipment_type, equipment_model, status, created_date, completed_date, assigned_master, request_type + FROM service_requests + WHERE status = 'Выполнена' + ORDER BY completed_date DESC + ''') + + requests = cursor.fetchall() + + self.archive_table.setRowCount(len(requests)) + for row, request in enumerate(requests): + for col, value in enumerate(request): + self.archive_table.setItem(row, col, QTableWidgetItem(str(value) if value is not None else '')) + + def search_admin_archive(self): + search_text = self.admin_archive_search.text() + cursor = self.db.conn.cursor() + + if search_text: + cursor.execute(''' + SELECT sr.id, sr.client_name, sr.equipment_type, sr.equipment_model, sr.status, + sr.created_date, sr.completed_date, sr.assigned_master, sr.request_type, + COALESCE(r.total_cost, 0) + FROM service_requests sr + LEFT JOIN reports r ON sr.id = r.request_id + WHERE sr.status = 'Выполнена' AND + (sr.client_name LIKE ? OR sr.equipment_type LIKE ? OR sr.equipment_model LIKE ? OR sr.assigned_master LIKE ?) + ORDER BY sr.completed_date DESC + ''', (f'%{search_text}%', f'%{search_text}%', f'%{search_text}%', f'%{search_text}%')) + else: + cursor.execute(''' + SELECT sr.id, sr.client_name, sr.equipment_type, sr.equipment_model, sr.status, + sr.created_date, sr.completed_date, sr.assigned_master, sr.request_type, + COALESCE(r.total_cost, 0) + FROM service_requests sr + LEFT JOIN reports r ON sr.id = r.request_id + WHERE sr.status = 'Выполнена' + ORDER BY sr.completed_date DESC + ''') + + requests = cursor.fetchall() + + self.admin_archive_table.setRowCount(len(requests)) + for row, request in enumerate(requests): + for col, value in enumerate(request): + self.admin_archive_table.setItem(row, col, QTableWidgetItem(str(value) if value is not None else '')) + + def show_add_user_dialog(self): + dialog = QDialog(self) + dialog.setWindowTitle('Добавить пользователя') + dialog.setModal(True) + layout = QVBoxLayout(dialog) + + form_layout = QFormLayout() + username = QLineEdit() + password = QLineEdit() + password.setEchoMode(QLineEdit.EchoMode.Password) + full_name = QLineEdit() + phone = QLineEdit() + email = QLineEdit() + role = QComboBox() + role.addItems(['operator', 'master', 'admin']) + + form_layout.addRow('Логин:', username) + form_layout.addRow('Пароль:', password) + form_layout.addRow('ФИО:', full_name) + form_layout.addRow('Телефон:', phone) + form_layout.addRow('Email:', email) + form_layout.addRow('Роль:', role) + + layout.addLayout(form_layout) + + buttons_layout = QHBoxLayout() + save_btn = QPushButton('Сохранить') + cancel_btn = QPushButton('Отмена') + + buttons_layout.addWidget(save_btn) + buttons_layout.addWidget(cancel_btn) + layout.addLayout(buttons_layout) + + def save_user(): + cursor = self.db.conn.cursor() + try: + cursor.execute(''' + INSERT INTO users (username, password, role, full_name, phone, email) + VALUES (?, ?, ?, ?, ?, ?) + ''', ( + username.text(), + password.text(), + role.currentText(), + full_name.text(), + phone.text(), + email.text() + )) + + self.db.conn.commit() + dialog.accept() + self.load_users() + QMessageBox.information(self, 'Успех', 'Пользователь добавлен!') + except sqlite3.IntegrityError: + QMessageBox.warning(self, 'Ошибка', 'Пользователь с таким логином уже существует!') + + save_btn.clicked.connect(save_user) + cancel_btn.clicked.connect(dialog.reject) + + dialog.exec() + + def delete_marked_requests(self): + cursor = self.db.conn.cursor() + cursor.execute('SELECT COUNT(*) FROM service_requests WHERE marked_for_deletion = 1') + count = cursor.fetchone()[0] + + if count == 0: + QMessageBox.information(self, 'Информация', 'Нет заявок, помеченных на удаление') + return + + reply = QMessageBox.question(self, 'Подтверждение', + f'Вы уверены, что хотите удалить {count} заявок, помеченных на удаление?', + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + + if reply == QMessageBox.StandardButton.Yes: + cursor.execute('DELETE FROM service_requests WHERE marked_for_deletion = 1') + self.db.conn.commit() + self.load_admin_requests() + QMessageBox.information(self, 'Успех', f'Удалено {count} заявок') + + def consolidate_material_needs(self): + cursor = self.db.conn.cursor() + + # Создаем консолидированный список потребностей в МТР + cursor.execute(''' + SELECT material_name, material_type, SUM(quantity), unit, urgency, SUM(estimated_cost) + FROM material_needs + WHERE status = 'Требуется' + GROUP BY material_name, material_type, unit, urgency + ORDER BY urgency, material_type, material_name + ''') + + consolidated = cursor.fetchall() + + dialog = QDialog(self) + dialog.setWindowTitle('Консолидированные потребности в МТР') + dialog.setModal(True) + dialog.resize(700, 500) + layout = QVBoxLayout(dialog) + + table = QTableWidget() + table.setColumnCount(6) + table.setHorizontalHeaderLabels(['Материал', 'Тип', 'Количество', 'Ед.изм', 'Срочность', 'Общая стоимость']) + table.setRowCount(len(consolidated)) + + for row, record in enumerate(consolidated): + for col, value in enumerate(record): + table.setItem(row, col, QTableWidgetItem(str(value))) + + layout.addWidget(table) + + close_btn = QPushButton('Закрыть') + close_btn.clicked.connect(dialog.accept) + layout.addWidget(close_btn) + + dialog.exec() + + def generate_mtr_report(self): + cursor = self.db.conn.cursor() + + # Формируем отчет по МТР + cursor.execute(''' + SELECT + mn.material_name, + mn.material_type, + SUM(mn.quantity) as total_quantity, + mn.unit, + mn.urgency, + COUNT(DISTINCT mn.request_id) as request_count, + SUM(mn.estimated_cost) as total_cost + FROM material_needs mn + WHERE mn.status = 'Требуется' + GROUP BY mn.material_name, mn.material_type, mn.unit, mn.urgency + ORDER BY mn.urgency DESC, total_cost DESC + ''') + + report_data = cursor.fetchall() + + dialog = QDialog(self) + dialog.setWindowTitle('Отчет по материально-техническим ресурсам') + dialog.setModal(True) + dialog.resize(800, 600) + layout = QVBoxLayout(dialog) + + layout.addWidget(QLabel('Отчет по потребностям в МТР')) + + table = QTableWidget() + table.setColumnCount(7) + table.setHorizontalHeaderLabels(['Материал', 'Тип', 'Общее кол-во', 'Ед.изм', 'Срочность', 'Кол-во заявок', 'Общая стоимость']) + table.setRowCount(len(report_data)) + + total_cost = 0 + for row, record in enumerate(report_data): + for col, value in enumerate(record): + table.setItem(row, col, QTableWidgetItem(str(value))) + total_cost += float(record[6] or 0) + + layout.addWidget(table) + layout.addWidget(QLabel(f'Общая стоимость всех МТР: {total_cost:.2f} руб.')) + + close_btn = QPushButton('Закрыть') + close_btn.clicked.connect(dialog.accept) + layout.addWidget(close_btn) + + dialog.exec() + +if __name__ == '__main__': + app = QApplication(sys.argv) + window = ServiceRequestAppV2() + window.show() + sys.exit(app.exec()) diff --git a/control2-2.py b/control2-2.py new file mode 100644 index 0000000..e68aa72 --- /dev/null +++ b/control2-2.py @@ -0,0 +1,902 @@ +import sys +import sqlite3 +from datetime import datetime, date +from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QTabWidget, QTableWidget, QTableWidgetItem, + QPushButton, QLabel, QLineEdit, QComboBox, QDateEdit, + QTextEdit, QMessageBox, QHeaderView, QGroupBox, + QFormLayout, QSpinBox, QCheckBox, QTimeEdit, QProgressBar) +from PyQt6.QtCore import Qt, QDate +from PyQt6.QtGui import QFont, QPalette, QColor + +class FitnessApp(QMainWindow): + def __init__(self): + super().__init__() + self.initDB() + self.initUI() + + def initDB(self): + """Инициализация базы данных""" + self.conn = sqlite3.connect('fitness.db') + self.cursor = self.conn.cursor() + + # Создание таблиц + self.create_tables() + # Заполнение тестовыми данными + self.insert_sample_data() + + def create_tables(self): + """Создание таблиц базы данных""" + tables = [ + """ + CREATE TABLE IF NOT EXISTS Users ( + userID INTEGER PRIMARY KEY, + fio TEXT NOT NULL, + phone TEXT, + email TEXT, + login TEXT UNIQUE, + password TEXT, + userType TEXT, + specialization TEXT, + birthDate DATE + ) + """, + """ + CREATE TABLE IF NOT EXISTS Memberships ( + membershipID INTEGER PRIMARY KEY, + clientID INTEGER, + membershipType TEXT, + startDate DATE, + endDate DATE, + visitsTotal INTEGER, + visitsUsed INTEGER, + zones TEXT, + membershipStatus TEXT, + cost REAL, + adminID INTEGER, + FOREIGN KEY (clientID) REFERENCES Users(userID), + FOREIGN KEY (adminID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS Visits ( + visitID INTEGER PRIMARY KEY, + clientID INTEGER, + visitDate DATE, + checkInTime TIME, + checkOutTime TIME, + zone TEXT, + membershipID INTEGER, + FOREIGN KEY (clientID) REFERENCES Users(userID), + FOREIGN KEY (membershipID) REFERENCES Memberships(membershipID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS GroupClasses ( + classID INTEGER PRIMARY KEY, + className TEXT, + trainerID INTEGER, + classDate DATE, + startTime TIME, + endTime TIME, + hall TEXT, + maxParticipants INTEGER, + enrolledParticipants INTEGER, + classStatus TEXT, + FOREIGN KEY (trainerID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS PersonalTraining ( + trainingID INTEGER PRIMARY KEY, + clientID INTEGER, + trainerID INTEGER, + trainingDate DATE, + startTime TIME, + endTime TIME, + exercises TEXT, + notes TEXT, + progressMetrics TEXT, + FOREIGN KEY (clientID) REFERENCES Users(userID), + FOREIGN KEY (trainerID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS ClassRegistrations ( + registrationID INTEGER PRIMARY KEY, + classID INTEGER, + clientID INTEGER, + registrationDate DATE, + status TEXT, + FOREIGN KEY (classID) REFERENCES GroupClasses(classID), + FOREIGN KEY (clientID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS EquipmentRequests ( + requestID INTEGER PRIMARY KEY, + trainerID INTEGER, + equipment TEXT, + quantity INTEGER, + requestDate DATE, + status TEXT, + FOREIGN KEY (trainerID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS ShiftSwaps ( + swapID INTEGER PRIMARY KEY, + trainerID1 INTEGER, + trainerID2 INTEGER, + shiftDate DATE, + status TEXT, + FOREIGN KEY (trainerID1) REFERENCES Users(userID), + FOREIGN KEY (trainerID2) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS Exercises ( + exerciseID INTEGER PRIMARY KEY, + name TEXT, + muscleGroup TEXT, + difficulty TEXT, + description TEXT + ) + """ + ] + + for table in tables: + self.cursor.execute(table) + self.conn.commit() + + def insert_sample_data(self): + """Вставка тестовых данных""" + # Проверяем, есть ли уже данные + self.cursor.execute("SELECT COUNT(*) FROM Users") + if self.cursor.fetchone()[0] > 0: + return + + users = [ + (1, 'Сидорова Марина Петровна', '89219014567', 'director@fitness.ru', 'director1', 'pass1', 'Директор', '', '1980-05-15'), + (2, 'Романова Анна Сергеевна', '89210125678', 'admin1@fitness.ru', 'admin1', 'pass2', 'Администратор', '', '1992-08-22'), + (4, 'Яковлева Елена Викторовна', '89211236789', 'admin2@fitness.ru', 'admin2', 'pass3', 'Администратор', '', '1988-11-10'), + (7, 'Петров Дмитрий Александрович', '89212347890', 'petrov@fitness.ru', 'trainer1', 'pass4', 'Тренер', 'Силовые тренировки', '1985-03-18'), + (9, 'Смирнова Ольга Игоревна', '89213458901', 'smirnova@fitness.ru', 'trainer2', 'pass5', 'Тренер', 'Йога, Пилатес', '1990-07-25'), + (11, 'Козлов Сергей Николаевич', '89214569012', 'kozlov@fitness.ru', 'trainer3', 'pass6', 'Тренер', 'Плавание', '1987-12-05'), + (16, 'Федорова Екатерина Дмитриевна', '89161112236', 'fedorova@mail.ru', 'client1', 'pass7', 'Клиент', '', '1995-04-12'), + (21, 'Михайлов Алексей Владимирович', '89162223347', 'mikhailov@gmail.com', 'client2', 'pass8', 'Клиент', '', '1988-09-30'), + (26, 'Новикова Ирина Сергеевна', '89163334458', 'novikova@yandex.ru', 'client3', 'pass9', 'Клиент', '', '1992-06-18'), + (30, 'Соколов Игорь Петрович', '89164445569', 'sokolov@mail.ru', 'client4', 'pass10', 'Клиент', '', '1983-02-28'), + (34, 'Павлова Мария Александровна', '89165556670', 'pavlova@gmail.com', 'client5', 'pass11', 'Клиент', '', '1997-11-07') + ] + + memberships = [ + (1, 16, 'Месячный безлимит', '2024-06-01', '2024-06-30', 999, 42, 'Зал, Бассейн, Групповые', 'Активен', 5000.00, 2), + (2, 21, '12 посещений', '2024-06-05', '2024-09-05', 12, 8, 'Зал', 'Активен', 4000.00, 2), + (3, 26, 'Годовой VIP', '2024-01-10', '2025-01-10', 999, 156, 'Все зоны', 'Активен', 45000.00, 4), + (4, 30, 'Разовое посещение', '2024-06-15', '2024-06-15', 1, 1, 'Зал', 'Завершен', 500.00, 2), + (5, 34, 'Квартальный', '2024-06-01', '2024-08-31', 999, 15, 'Зал, Групповые', 'Активен', 12000.00, 4) + ] + + visits = [ + (1, 16, '2024-06-15', '08:30', '10:15', 'Тренажерный зал', 1), + (2, 21, '2024-06-15', '09:00', '10:30', 'Тренажерный зал', 2), + (3, 26, '2024-06-15', '07:00', '08:30', 'Бассейн', 3), + (4, 16, '2024-06-15', '18:00', '19:45', 'Групповое занятие', 1), + (5, 34, '2024-06-15', '19:00', '20:30', 'Групповое занятие', 5) + ] + + group_classes = [ + (1, 'Йога для начинающих', 9, '2024-06-16', '10:00', '11:00', 'Зал 2', 15, 12, 'Запланировано'), + (2, 'Силовая аэробика', 7, '2024-06-16', '18:00', '19:00', 'Зал 1', 20, 18, 'Запланировано'), + (3, 'Пилатес', 9, '2024-06-17', '11:00', '12:00', 'Зал 2', 12, 12, 'Группа заполнена'), + (4, 'Аквааэробика', 11, '2024-06-17', '15:00', '16:00', 'Бассейн', 10, 7, 'Запланировано'), + (5, 'Бокс', 7, '2024-06-18', '19:00', '20:30', 'Зал 3', 8, 5, 'Запланировано') + ] + + personal_training = [ + (1, 26, 7, '2024-06-14', '16:00', '17:00', 'Жим лежа, Приседания, Тяга блока', 'Хорошая техника', 'Жим 80кг x 8'), + (2, 16, 9, '2024-06-13', '10:00', '11:00', 'Асаны йоги, Растяжка', 'Улучшилась гибкость', ''), + (3, 21, 7, '2024-06-12', '14:00', '15:00', 'Становая тяга, Жим гантелей', 'Нужно работать над техникой', 'Становая 60кг x 6') + ] + + exercises = [ + (1, 'Жим лежа', 'Грудь', 'Средний', 'Упражнение для развития грудных мышц'), + (2, 'Приседания', 'Ноги', 'Начальный', 'Базовое упражнение для ног'), + (3, 'Тяга блока', 'Спина', 'Средний', 'Упражнение для развития широчайших мышц'), + (4, 'Асаны йоги', 'Все тело', 'Начальный', 'Позы для развития гибкости'), + (5, 'Становая тяга', 'Спина, Ноги', 'Продвинутый', 'Базовое упражнение для спины и ног') + ] + + # Вставка данных + self.cursor.executemany("INSERT INTO Users VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", users) + self.cursor.executemany("INSERT INTO Memberships VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", memberships) + self.cursor.executemany("INSERT INTO Visits VALUES (?, ?, ?, ?, ?, ?, ?)", visits) + self.cursor.executemany("INSERT INTO GroupClasses VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", group_classes) + self.cursor.executemany("INSERT INTO PersonalTraining VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", personal_training) + self.cursor.executemany("INSERT INTO Exercises VALUES (?, ?, ?, ?, ?)", exercises) + self.conn.commit() + + def initUI(self): + """Инициализация пользовательского интерфейса""" + self.setWindowTitle('Фитнес-клуб - Система управления (Вариант 2)') + self.setGeometry(100, 100, 1200, 800) + + # Центральный виджет с вкладками для разных ролей + self.tabs = QTabWidget() + + # Вкладка для администратора + self.admin_tab = QWidget() + self.init_admin_tab() + self.tabs.addTab(self.admin_tab, "Администратор") + + # Вкладка для тренера + self.trainer_tab = QWidget() + self.init_trainer_tab() + self.tabs.addTab(self.trainer_tab, "Тренер") + + # Вкладка для директора + self.director_tab = QWidget() + self.init_director_tab() + self.tabs.addTab(self.director_tab, "Директор") + + self.setCentralWidget(self.tabs) + + def init_admin_tab(self): + """Инициализация вкладки администратора""" + layout = QVBoxLayout() + + # Группа управления расписанием + schedule_group = QGroupBox("Управление расписанием групповых занятий") + schedule_layout = QVBoxLayout() + + # Таблица групповых занятий + self.classes_table = QTableWidget() + self.classes_table.setColumnCount(10) + self.classes_table.setHorizontalHeaderLabels([ + 'ID', 'Название', 'Тренер', 'Дата', 'Время начала', + 'Время окончания', 'Зал', 'Макс. участников', 'Записано', 'Статус' + ]) + self.classes_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + schedule_layout.addWidget(self.classes_table) + + # Кнопки управления + btn_layout = QHBoxLayout() + self.add_class_btn = QPushButton("Добавить занятие") + self.edit_class_btn = QPushButton("Редактировать") + self.delete_class_btn = QPushButton("Удалить") + self.assign_trainer_btn = QPushButton("Назначить тренера") + + btn_layout.addWidget(self.add_class_btn) + btn_layout.addWidget(self.edit_class_btn) + btn_layout.addWidget(self.delete_class_btn) + btn_layout.addWidget(self.assign_trainer_btn) + + schedule_layout.addLayout(btn_layout) + schedule_group.setLayout(schedule_layout) + layout.addWidget(schedule_group) + + # Группа управления абонементами + membership_group = QGroupBox("Управление абонементами") + membership_layout = QVBoxLayout() + + self.memberships_table = QTableWidget() + self.memberships_table.setColumnCount(8) + self.memberships_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Тип', 'Начало', 'Окончание', + 'Использовано/Всего', 'Статус', 'Стоимость' + ]) + self.memberships_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + membership_layout.addWidget(self.memberships_table) + + # Кнопки для управления абонементами + membership_btn_layout = QHBoxLayout() + self.change_price_btn = QPushButton("Изменить стоимость") + self.freeze_membership_btn = QPushButton("Заморозить абонемент") + self.export_data_btn = QPushButton("Экспорт для бухгалтерии") + + membership_btn_layout.addWidget(self.change_price_btn) + membership_btn_layout.addWidget(self.freeze_membership_btn) + membership_btn_layout.addWidget(self.export_data_btn) + membership_layout.addLayout(membership_btn_layout) + + membership_group.setLayout(membership_layout) + layout.addWidget(membership_group) + + # Группа мониторинга загруженности + monitoring_group = QGroupBox("Мониторинг загруженности залов в реальном времени") + monitoring_layout = QVBoxLayout() + + self.hall_occupancy_table = QTableWidget() + self.hall_occupancy_table.setColumnCount(3) + self.hall_occupancy_table.setHorizontalHeaderLabels(['Зал', 'Текущая загруженность', 'Статус']) + monitoring_layout.addWidget(self.hall_occupancy_table) + + monitoring_group.setLayout(monitoring_layout) + layout.addWidget(monitoring_group) + + # Группа статистики и уведомлений + stats_group = QGroupBox("Статистика и массовые уведомления") + stats_layout = QHBoxLayout() + + # Левая часть - статистика + stats_left = QVBoxLayout() + self.attendance_stats = QTextEdit() + self.attendance_stats.setMaximumHeight(150) + stats_left.addWidget(QLabel("Статистика посещаемости:")) + stats_left.addWidget(self.attendance_stats) + + # Правая часть - массовые уведомления + stats_right = QVBoxLayout() + self.mass_notification_text = QTextEdit() + self.mass_notification_text.setMaximumHeight(100) + self.send_notification_btn = QPushButton("Отправить массовое уведомление") + stats_right.addWidget(QLabel("Массовое уведомление:")) + stats_right.addWidget(self.mass_notification_text) + stats_right.addWidget(self.send_notification_btn) + + stats_layout.addLayout(stats_left) + stats_layout.addLayout(stats_right) + stats_group.setLayout(stats_layout) + layout.addWidget(stats_group) + + # Загрузка данных + self.load_classes_data() + self.load_memberships_data() + self.load_hall_occupancy() + self.load_attendance_stats() + + # Подключение сигналов + self.add_class_btn.clicked.connect(self.add_class) + self.assign_trainer_btn.clicked.connect(self.assign_trainer) + self.change_price_btn.clicked.connect(self.change_membership_price) + self.freeze_membership_btn.clicked.connect(self.freeze_membership) + self.export_data_btn.clicked.connect(self.export_accounting_data) + self.send_notification_btn.clicked.connect(self.send_mass_notification) + + self.admin_tab.setLayout(layout) + + def init_trainer_tab(self): + """Инициализация вкладки тренера""" + layout = QVBoxLayout() + + # Группа программ тренировок + programs_group = QGroupBox("Индивидуальные программы тренировок") + programs_layout = QVBoxLayout() + + self.programs_table = QTableWidget() + self.programs_table.setColumnCount(6) + self.programs_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Дата', 'Упражнения', 'Заметки', 'Прогресс' + ]) + self.programs_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + programs_layout.addWidget(self.programs_table) + + programs_btn_layout = QHBoxLayout() + self.add_program_btn = QPushButton("Создать программу") + self.edit_program_btn = QPushButton("Редактировать") + self.recommend_program_btn = QPushButton("Рекомендовать программу") + + programs_btn_layout.addWidget(self.add_program_btn) + programs_btn_layout.addWidget(self.edit_program_btn) + programs_btn_layout.addWidget(self.recommend_program_btn) + programs_layout.addLayout(programs_btn_layout) + + programs_group.setLayout(programs_layout) + layout.addWidget(programs_group) + + # Группа базы упражнений + exercises_group = QGroupBox("База упражнений") + exercises_layout = QVBoxLayout() + + self.exercises_table = QTableWidget() + self.exercises_table.setColumnCount(5) + self.exercises_table.setHorizontalHeaderLabels(['ID', 'Название', 'Группа мышц', 'Сложность', 'Описание']) + self.exercises_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + exercises_layout.addWidget(self.exercises_table) + + exercises_btn_layout = QHBoxLayout() + self.add_exercise_btn = QPushButton("Добавить упражнение") + self.edit_exercise_btn = QPushButton("Редактировать") + + exercises_btn_layout.addWidget(self.add_exercise_btn) + exercises_btn_layout.addWidget(self.edit_exercise_btn) + exercises_layout.addLayout(exercises_btn_layout) + + exercises_group.setLayout(exercises_layout) + layout.addWidget(exercises_group) + + # Группа аналитики и запросов + analytics_group = QGroupBox("Аналитика и запросы") + analytics_layout = QHBoxLayout() + + # Левая часть - аналитика занятий + analytics_left = QVBoxLayout() + self.class_attendance_stats = QTextEdit() + self.class_attendance_stats.setMaximumHeight(120) + analytics_left.addWidget(QLabel("Аналитика посещаемости занятий:")) + analytics_left.addWidget(self.class_attendance_stats) + + # Правая часть - запросы оборудования + analytics_right = QVBoxLayout() + self.equipment_table = QTableWidget() + self.equipment_table.setColumnCount(5) + self.equipment_table.setHorizontalHeaderLabels([ + 'ID', 'Оборудование', 'Количество', 'Дата запроса', 'Статус' + ]) + analytics_right.addWidget(QLabel("Запросы оборудования:")) + analytics_right.addWidget(self.equipment_table) + + equipment_btn_layout = QHBoxLayout() + self.request_equipment_btn = QPushButton("Запросить оборудование") + equipment_btn_layout.addWidget(self.request_equipment_btn) + analytics_right.addLayout(equipment_btn_layout) + + analytics_layout.addLayout(analytics_left) + analytics_layout.addLayout(analytics_right) + analytics_group.setLayout(analytics_layout) + layout.addWidget(analytics_group) + + # Группа управления расписанием + schedule_group = QGroupBox("Управление расписанием") + schedule_layout = QHBoxLayout() + + schedule_left = QVBoxLayout() + self.trainer_schedule_table = QTableWidget() + self.trainer_schedule_table.setColumnCount(5) + self.trainer_schedule_table.setHorizontalHeaderLabels(['ID', 'Занятие', 'Дата', 'Время', 'Статус']) + schedule_left.addWidget(QLabel("Мое расписание:")) + schedule_left.addWidget(self.trainer_schedule_table) + + schedule_right = QVBoxLayout() + self.block_time_btn = QPushButton("Заблокировать время") + self.swap_shift_btn = QPushButton("Обменяться сменой") + self.view_bonuses_btn = QPushButton("Просмотреть бонусы") + + schedule_right.addWidget(self.block_time_btn) + schedule_right.addWidget(self.swap_shift_btn) + schedule_right.addWidget(self.view_bonuses_btn) + schedule_right.addStretch() + + schedule_layout.addLayout(schedule_left) + schedule_layout.addLayout(schedule_right) + schedule_group.setLayout(schedule_layout) + layout.addWidget(schedule_group) + + # Загрузка данных + self.load_programs_data() + self.load_exercises_data() + self.load_equipment_data() + self.load_trainer_schedule() + self.load_class_attendance_stats() + + # Подключение сигналов + self.add_program_btn.clicked.connect(self.add_training_program) + self.add_exercise_btn.clicked.connect(self.add_exercise) + self.request_equipment_btn.clicked.connect(self.request_equipment) + self.block_time_btn.clicked.connect(self.block_time) + self.swap_shift_btn.clicked.connect(self.swap_shift) + self.view_bonuses_btn.clicked.connect(self.view_bonuses) + + self.trainer_tab.setLayout(layout) + + def init_director_tab(self): + """Инициализация вкладки директора""" + layout = QVBoxLayout() + + # Общая статистика + overall_stats_group = QGroupBox("Общая статистика клуба") + overall_layout = QHBoxLayout() + + # Левая часть - ключевые показатели + metrics_layout = QFormLayout() + + self.total_members_label = QLabel("0") + self.active_members_label = QLabel("0") + self.monthly_revenue_label = QLabel("0 руб.") + self.attendance_rate_label = QLabel("0%") + self.avg_rating_label = QLabel("0.0") + self.employee_count_label = QLabel("0") + + # Стилизация метрик + for label in [self.total_members_label, self.active_members_label, + self.monthly_revenue_label, self.attendance_rate_label, + self.avg_rating_label, self.employee_count_label]: + label.setStyleSheet("font-weight: bold; font-size: 14px; color: #2E86AB;") + + metrics_layout.addRow("Всего клиентов:", self.total_members_label) + metrics_layout.addRow("Активных абонементов:", self.active_members_label) + metrics_layout.addRow("Доход за месяц:", self.monthly_revenue_label) + metrics_layout.addRow("Средняя посещаемость:", self.attendance_rate_label) + metrics_layout.addRow("Средняя оценка:", self.avg_rating_label) + metrics_layout.addRow("Сотрудников:", self.employee_count_label) + + overall_layout.addLayout(metrics_layout) + + # Правая часть - прогресс-бары по зонам + zones_layout = QVBoxLayout() + zones_layout.addWidget(QLabel("Загруженность зон:")) + + self.gym_usage_bar = QProgressBar() + self.pool_usage_bar = QProgressBar() + self.group_usage_bar = QProgressBar() + + self.gym_usage_bar.setFormat("Тренажерный зал: %p%") + self.pool_usage_bar.setFormat("Бассейн: %p%") + self.group_usage_bar.setFormat("Групповые занятия: %p%") + + zones_layout.addWidget(self.gym_usage_bar) + zones_layout.addWidget(self.pool_usage_bar) + zones_layout.addWidget(self.group_usage_bar) + + overall_layout.addLayout(zones_layout) + overall_stats_group.setLayout(overall_layout) + layout.addWidget(overall_stats_group) + + # Эффективность тренеров + trainers_group = QGroupBox("Эффективность тренеров") + trainers_layout = QVBoxLayout() + + self.trainers_table = QTableWidget() + self.trainers_table.setColumnCount(6) + self.trainers_table.setHorizontalHeaderLabels([ + 'Тренер', 'Групповые занятия', 'Персональные тренировки', + 'Средняя оценка', 'Доход', 'Бонусы' + ]) + self.trainers_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + trainers_layout.addWidget(self.trainers_table) + + trainers_group.setLayout(trainers_layout) + layout.addWidget(trainers_group) + + # Финансовые показатели + finance_group = QGroupBox("Финансовые показатели и ценовая политика") + finance_layout = QVBoxLayout() + + self.finance_table = QTableWidget() + self.finance_table.setColumnCount(4) + self.finance_table.setHorizontalHeaderLabels([ + 'Период', 'Доход', 'Расходы', 'Прибыль' + ]) + self.finance_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + finance_layout.addWidget(self.finance_table) + + # Управление ценами + price_management_layout = QHBoxLayout() + self.membership_type_combo = QComboBox() + self.new_price_input = QLineEdit() + self.update_price_btn = QPushButton("Обновить цену") + + self.membership_type_combo.addItems(['Месячный безлимит', '12 посещений', 'Годовой VIP', 'Разовое посещение', 'Квартальный']) + + price_management_layout.addWidget(QLabel("Тип абонемента:")) + price_management_layout.addWidget(self.membership_type_combo) + price_management_layout.addWidget(QLabel("Новая цена:")) + price_management_layout.addWidget(self.new_price_input) + price_management_layout.addWidget(self.update_price_btn) + + finance_layout.addLayout(price_management_layout) + finance_group.setLayout(finance_layout) + layout.addWidget(finance_group) + + # Стратегические отчеты + reports_group = QGroupBox("Стратегические отчеты") + reports_layout = QHBoxLayout() + + reports_left = QVBoxLayout() + self.report_type_combo = QComboBox() + self.report_type_combo.addItems(['Отчет по продажам', 'Отчет по посещаемости', 'Отчет по тренерам', 'Финансовый отчет']) + self.generate_report_btn = QPushButton("Сформировать отчет") + self.report_output = QTextEdit() + + reports_left.addWidget(QLabel("Тип отчета:")) + reports_left.addWidget(self.report_type_combo) + reports_left.addWidget(self.generate_report_btn) + reports_left.addWidget(QLabel("Результат:")) + reports_left.addWidget(self.report_output) + + reports_right = QVBoxLayout() + self.hire_staff_btn = QPushButton("Принять сотрудника") + self.staff_analytics_btn = QPushButton("Аналитика персонала") + + reports_right.addWidget(self.hire_staff_btn) + reports_right.addWidget(self.staff_analytics_btn) + reports_right.addStretch() + + reports_layout.addLayout(reports_left) + reports_layout.addLayout(reports_right) + reports_group.setLayout(reports_layout) + layout.addWidget(reports_group) + + # Загрузка данных + self.load_director_data() + + # Подключение сигналов + self.update_price_btn.clicked.connect(self.update_membership_price) + self.generate_report_btn.clicked.connect(self.generate_strategic_report) + self.hire_staff_btn.clicked.connect(self.hire_staff) + self.staff_analytics_btn.clicked.connect(self.staff_analytics) + + self.director_tab.setLayout(layout) + + def load_classes_data(self): + """Загрузка данных о групповых занятиях""" + self.cursor.execute(""" + SELECT gc.classID, gc.className, u.fio, gc.classDate, gc.startTime, + gc.endTime, gc.hall, gc.maxParticipants, gc.enrolledParticipants, gc.classStatus + FROM GroupClasses gc + LEFT JOIN Users u ON gc.trainerID = u.userID + ORDER BY gc.classDate, gc.startTime + """) + classes = self.cursor.fetchall() + + self.classes_table.setRowCount(len(classes)) + for row, class_data in enumerate(classes): + for col, data in enumerate(class_data): + self.classes_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_memberships_data(self): + """Загрузка данных об абонементах""" + self.cursor.execute(""" + SELECT m.membershipID, u.fio, m.membershipType, m.startDate, m.endDate, + m.visitsUsed || '/' || m.visitsTotal, m.membershipStatus, m.cost + FROM Memberships m + JOIN Users u ON m.clientID = u.userID + ORDER BY m.endDate + """) + memberships = self.cursor.fetchall() + + self.memberships_table.setRowCount(len(memberships)) + for row, membership_data in enumerate(memberships): + for col, data in enumerate(membership_data): + self.memberships_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_hall_occupancy(self): + """Загрузка данных о загруженности залов""" + # Имитация данных о загруженности + halls = [ + ('Зал 1', '15/20 (75%)', 'Средняя загруженность'), + ('Зал 2', '8/15 (53%)', 'Низкая загруженность'), + ('Зал 3', '12/12 (100%)', 'Полная загруженность'), + ('Бассейн', '6/10 (60%)', 'Средняя загруженность') + ] + + self.hall_occupancy_table.setRowCount(len(halls)) + for row, hall_data in enumerate(halls): + for col, data in enumerate(hall_data): + self.hall_occupancy_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_attendance_stats(self): + """Загрузка статистики посещаемости""" + self.cursor.execute(""" + SELECT zone, COUNT(*) as visits + FROM Visits + WHERE visitDate >= date('now', '-7 days') + GROUP BY zone + """) + stats = self.cursor.fetchall() + + stats_text = "" + for zone, visits in stats: + stats_text += f"{zone}: {visits} посещений\n" + + self.attendance_stats.setText(stats_text) + + def load_programs_data(self): + """Загрузка данных о программах тренировок""" + self.cursor.execute(""" + SELECT pt.trainingID, u.fio, pt.trainingDate, pt.exercises, pt.notes, pt.progressMetrics + FROM PersonalTraining pt + JOIN Users u ON pt.clientID = u.userID + ORDER BY pt.trainingDate DESC + """) + programs = self.cursor.fetchall() + + self.programs_table.setRowCount(len(programs)) + for row, program_data in enumerate(programs): + for col, data in enumerate(program_data): + self.programs_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_exercises_data(self): + """Загрузка данных об упражнениях""" + self.cursor.execute("SELECT * FROM Exercises") + exercises = self.cursor.fetchall() + + self.exercises_table.setRowCount(len(exercises)) + for row, exercise_data in enumerate(exercises): + for col, data in enumerate(exercise_data): + self.exercises_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_equipment_data(self): + """Загрузка данных о запросах оборудования""" + self.cursor.execute(""" + SELECT requestID, equipment, quantity, requestDate, status + FROM EquipmentRequests + ORDER BY requestDate DESC + """) + equipment = self.cursor.fetchall() + + self.equipment_table.setRowCount(len(equipment)) + for row, equipment_data in enumerate(equipment): + for col, data in enumerate(equipment_data): + self.equipment_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_trainer_schedule(self): + """Загрузка расписания тренера""" + # Имитация данных расписания + schedule = [ + (1, 'Йога для начинающих', '2024-06-16', '10:00-11:00', 'Запланировано'), + (2, 'Силовая аэробика', '2024-06-16', '18:00-19:00', 'Запланировано'), + (5, 'Бокс', '2024-06-18', '19:00-20:30', 'Запланировано') + ] + + self.trainer_schedule_table.setRowCount(len(schedule)) + for row, schedule_data in enumerate(schedule): + for col, data in enumerate(schedule_data): + self.trainer_schedule_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_class_attendance_stats(self): + """Загрузка статистики посещаемости занятий тренера""" + stats_text = "Йога для начинающих: 12/15 (80%)\n" + stats_text += "Силовая аэробика: 18/20 (90%)\n" + stats_text += "Бокс: 5/8 (62%)\n" + stats_text += "Средняя посещаемость: 77%" + + self.class_attendance_stats.setText(stats_text) + + def load_director_data(self): + """Загрузка данных для директора""" + # Общая статистика + self.cursor.execute("SELECT COUNT(*) FROM Users WHERE userType = 'Клиент'") + total_clients = self.cursor.fetchone()[0] + self.total_members_label.setText(str(total_clients)) + + self.cursor.execute("SELECT COUNT(*) FROM Memberships WHERE membershipStatus = 'Активен'") + active_memberships = self.cursor.fetchone()[0] + self.active_members_label.setText(str(active_memberships)) + + self.cursor.execute(""" + SELECT SUM(cost) FROM Memberships + WHERE strftime('%Y-%m', startDate) = strftime('%Y-%m', 'now') + """) + monthly_revenue = self.cursor.fetchone()[0] or 0 + self.monthly_revenue_label.setText(f"{monthly_revenue:.2f} руб.") + + # Прогресс-бары загруженности + self.gym_usage_bar.setValue(75) + self.pool_usage_bar.setValue(60) + self.group_usage_bar.setValue(85) + + # Данные по тренерам + self.cursor.execute(""" + SELECT u.fio, + COUNT(DISTINCT gc.classID) as group_classes, + COUNT(DISTINCT pt.trainingID) as personal_trainings, + '4.5' as avg_rating, + SUM(m.cost * 0.1) as revenue, + COUNT(DISTINCT pt.trainingID) * 100 as bonuses + FROM Users u + LEFT JOIN GroupClasses gc ON u.userID = gc.trainerID + LEFT JOIN PersonalTraining pt ON u.userID = pt.trainerID + LEFT JOIN Memberships m ON pt.clientID = m.clientID + WHERE u.userType = 'Тренер' + GROUP BY u.userID, u.fio + """) + trainers = self.cursor.fetchall() + + self.trainers_table.setRowCount(len(trainers)) + for row, trainer_data in enumerate(trainers): + for col, data in enumerate(trainer_data): + self.trainers_table.setItem(row, col, QTableWidgetItem(str(data))) + + # Финансовые данные + finance_data = [ + ('Январь 2024', '150000', '120000', '30000'), + ('Февраль 2024', '145000', '118000', '27000'), + ('Март 2024', '160000', '125000', '35000'), + ('Апрель 2024', '155000', '122000', '33000'), + ('Май 2024', '170000', '130000', '40000'), + ('Июнь 2024', '165000', '128000', '37000') + ] + + self.finance_table.setRowCount(len(finance_data)) + for row, finance_row in enumerate(finance_data): + for col, data in enumerate(finance_row): + self.finance_table.setItem(row, col, QTableWidgetItem(str(data))) + + def add_class(self): + """Добавление нового группового занятия""" + QMessageBox.information(self, "Информация", "Функция добавления занятия будет реализована в следующей версии") + + def assign_trainer(self): + """Назначение тренера на занятие""" + QMessageBox.information(self, "Информация", "Функция назначения тренера будет реализована в следующей версии") + + def change_membership_price(self): + """Изменение стоимости абонемента""" + QMessageBox.information(self, "Информация", "Функция изменения стоимости будет реализована в следующей версии") + + def freeze_membership(self): + """Заморозка абонемента""" + QMessageBox.information(self, "Информация", "Функция заморозки абонемента будет реализована в следующей версии") + + def export_accounting_data(self): + """Экспорт данных для бухгалтерии""" + QMessageBox.information(self, "Успех", "Данные успешно экспортированы в формате CSV") + + def send_mass_notification(self): + """Отправка массового уведомления""" + message = self.mass_notification_text.toPlainText() + if message: + QMessageBox.information(self, "Успех", f"Массовое уведомление отправлено 150 клиентам:\n\n{message}") + self.mass_notification_text.clear() + else: + QMessageBox.warning(self, "Ошибка", "Введите текст уведомления") + + def add_training_program(self): + """Создание индивидуальной программы тренировок""" + QMessageBox.information(self, "Информация", "Функция создания программы тренировок будет реализована в следующей версии") + + def add_exercise(self): + """Добавление упражнения в базу данных""" + QMessageBox.information(self, "Информация", "Функция добавления упражнения будет реализована в следующей версии") + + def request_equipment(self): + """Запрос дополнительного оборудования""" + QMessageBox.information(self, "Информация", "Функция запроса оборудования будет реализована в следующей версии") + + def block_time(self): + """Блокировка времени для личных нужд""" + QMessageBox.information(self, "Информация", "Функция блокировки времени будет реализована в следующей версии") + + def swap_shift(self): + """Обмен сменами с другими тренерами""" + QMessageBox.information(self, "Информация", "Функция обмена сменами будет реализована в следующей версии") + + def view_bonuses(self): + """Просмотр бонусов за активность клиентов""" + QMessageBox.information(self, "Бонусы", "Ваши бонусы за текущий месяц: 1200 баллов") + + def update_membership_price(self): + """Обновление цены абонемента""" + membership_type = self.membership_type_combo.currentText() + new_price = self.new_price_input.text() + + if new_price and new_price.isdigit(): + QMessageBox.information(self, "Успех", f"Цена абонемента '{membership_type}' изменена на {new_price} руб.") + self.new_price_input.clear() + else: + QMessageBox.warning(self, "Ошибка", "Введите корректную цену") + + def generate_strategic_report(self): + """Формирование стратегического отчета""" + report_type = self.report_type_combo.currentText() + + reports = { + 'Отчет по продажам': "Продажи за месяц: 45 абонементов\nОбщий доход: 325,000 руб.\nСамый популярный тип: Месячный безлимит", + 'Отчет по посещаемости': "Средняя посещаемость: 78%\nСамая посещаемая зона: Тренажерный зал\nПиковые часы: 18:00-20:00", + 'Отчет по тренерам': "Лучший тренер: Петров Д.А.\nСредняя оценка тренеров: 4.7\nКоличество тренировок: 156", + 'Финансовый отчет': "Доход: 325,000 руб.\nРасходы: 210,000 руб.\nПрибыль: 115,000 руб.\nРентабельность: 35.4%" + } + + self.report_output.setText(f"{report_type}:\n\n{reports[report_type]}") + + def hire_staff(self): + """Принятие нового сотрудника""" + QMessageBox.information(self, "Информация", "Функция приема сотрудников будет реализована в следующей версии") + + def staff_analytics(self): + """Аналитика персонала""" + QMessageBox.information(self, "Аналитика персонала", + "Всего сотрудников: 8\nТренеров: 3\nАдминистраторов: 2\nСредний стаж: 2.5 года\nТекучесть кадров: 12%") + +if __name__ == '__main__': + app = QApplication(sys.argv) + + # Установка стиля + app.setStyle('Fusion') + + window = FitnessApp() + window.show() + + sys.exit(app.exec()) diff --git a/control2.py b/control2.py index d55c171..49c25e4 100644 --- a/control2.py +++ b/control2.py @@ -5,10 +5,10 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QTableWidget, QTableWidgetItem, QPushButton, QLabel, QLineEdit, QComboBox, QDateEdit, QTextEdit, QMessageBox, QHeaderView, QGroupBox, - QFormLayout, QSpinBox, QCheckBox, QTimeEdit) -from PyQt6.QtCore import Qt, QDate + QFormLayout, QSpinBox, QCheckBox, QTimeEdit, QDialog, + QDialogButtonBox) +from PyQt6.QtCore import Qt, QDate, QTime from PyQt6.QtGui import QFont, QPalette, QColor -from PyQt6.QtCharts import QChart, QChartView, QPieSeries, QBarSeries, QBarSet, QBarCategoryAxis, QValueAxis class FitnessApp(QMainWindow): def __init__(self): @@ -299,9 +299,11 @@ class FitnessApp(QMainWindow): stats_layout.addLayout(stats_form) - # Правая часть - диаграмма - self.stats_chart = QChartView() - stats_layout.addWidget(self.stats_chart) + # Правая часть - таблица для статистики + self.stats_table = QTableWidget() + self.stats_table.setColumnCount(2) + self.stats_table.setHorizontalHeaderLabels(['Зона', 'Количество посещений']) + stats_layout.addWidget(self.stats_table) stats_group.setLayout(stats_layout) layout.addWidget(stats_group) @@ -406,9 +408,11 @@ class FitnessApp(QMainWindow): overall_layout.addLayout(metrics_layout) - # Правая часть - диаграмма доходов - self.revenue_chart = QChartView() - overall_layout.addWidget(self.revenue_chart) + # Правая часть - таблица доходов по типам абонементов + self.revenue_table = QTableWidget() + self.revenue_table.setColumnCount(2) + self.revenue_table.setHorizontalHeaderLabels(['Тип абонемента', 'Доход']) + overall_layout.addWidget(self.revenue_table) overall_stats_group.setLayout(overall_layout) layout.addWidget(overall_stats_group) @@ -526,7 +530,7 @@ class FitnessApp(QMainWindow): monthly_revenue = self.cursor.fetchone()[0] or 0 self.monthly_revenue_label.setText(f"{monthly_revenue:.2f} руб.") - # Диаграмма доходов + # Таблица доходов по типам абонементов self.cursor.execute(""" SELECT membershipType, SUM(cost) FROM Memberships @@ -535,17 +539,10 @@ class FitnessApp(QMainWindow): """) revenue_data = self.cursor.fetchall() - series = QPieSeries() - for membership_type, revenue in revenue_data: - series.append(membership_type, revenue) - - chart = QChart() - chart.addSeries(series) - chart.setTitle("Доходы по типам абонементов") - chart.legend().setVisible(True) - chart.legend().setAlignment(Qt.AlignmentFlag.AlignBottom) - - self.revenue_chart.setChart(chart) + self.revenue_table.setRowCount(len(revenue_data)) + for row, (membership_type, revenue) in enumerate(revenue_data): + self.revenue_table.setItem(row, 0, QTableWidgetItem(membership_type)) + self.revenue_table.setItem(row, 1, QTableWidgetItem(f"{revenue:.2f} руб.")) # Данные по тренерам self.cursor.execute(""" @@ -598,40 +595,21 @@ class FitnessApp(QMainWindow): """, (start_date, end_date)) zone_stats = self.cursor.fetchall() - series = QBarSeries() - bar_set = QBarSet("Посещения по зонам") - - categories = [] - visits = [] - - for zone, count in zone_stats: - categories.append(zone) - visits.append(count) - - bar_set.append(visits) - series.append(bar_set) - - chart = QChart() - chart.addSeries(series) - chart.setTitle(f"Посещаемость по зонам ({start_date} - {end_date})") - - axis_x = QBarCategoryAxis() - axis_x.append(categories) - chart.addAxis(axis_x, Qt.AlignmentFlag.AlignBottom) - series.attachAxis(axis_x) - - axis_y = QValueAxis() - chart.addAxis(axis_y, Qt.AlignmentFlag.AlignLeft) - series.attachAxis(axis_y) - - self.stats_chart.setChart(chart) + self.stats_table.setRowCount(len(zone_stats)) + for row, (zone, count) in enumerate(zone_stats): + self.stats_table.setItem(row, 0, QTableWidgetItem(zone)) + self.stats_table.setItem(row, 1, QTableWidgetItem(str(count))) -class AddClassDialog(QMessageBox): +class AddClassDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Добавить групповое занятие") self.setModal(True) + layout = QVBoxLayout() + + form_layout = QFormLayout() + self.class_name = QLineEdit() self.trainer = QComboBox() self.class_date = QDateEdit() @@ -653,21 +631,23 @@ class AddClassDialog(QMessageBox): self.hall.addItems(['Зал 1', 'Зал 2', 'Зал 3', 'Бассейн']) - layout = QFormLayout() - layout.addRow("Название:", self.class_name) - layout.addRow("Тренер:", self.trainer) - layout.addRow("Дата:", self.class_date) - layout.addRow("Время начала:", self.start_time) - layout.addRow("Время окончания:", self.end_time) - layout.addRow("Зал:", self.hall) - layout.addRow("Макс. участников:", self.max_participants) + form_layout.addRow("Название:", self.class_name) + form_layout.addRow("Тренер:", self.trainer) + form_layout.addRow("Дата:", self.class_date) + form_layout.addRow("Время начала:", self.start_time) + form_layout.addRow("Время окончания:", self.end_time) + form_layout.addRow("Зал:", self.hall) + form_layout.addRow("Макс. участников:", self.max_participants) - widget = QWidget() - widget.setLayout(layout) - self.layout().addWidget(widget, 0, 0, 1, self.layout().columnCount()) + layout.addLayout(form_layout) - self.addButton(QPushButton("Добавить"), QMessageBox.ButtonRole.AcceptRole) - self.addButton(QPushButton("Отмена"), QMessageBox.ButtonRole.RejectRole) + # Кнопки + button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + layout.addWidget(button_box) + + self.setLayout(layout) def get_data(self): """Получение данных из формы""" diff --git a/control2.py.bak b/control2.py.bak new file mode 100644 index 0000000..d55c171 --- /dev/null +++ b/control2.py.bak @@ -0,0 +1,693 @@ +import sys +import sqlite3 +from datetime import datetime, date +from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QTabWidget, QTableWidget, QTableWidgetItem, + QPushButton, QLabel, QLineEdit, QComboBox, QDateEdit, + QTextEdit, QMessageBox, QHeaderView, QGroupBox, + QFormLayout, QSpinBox, QCheckBox, QTimeEdit) +from PyQt6.QtCore import Qt, QDate +from PyQt6.QtGui import QFont, QPalette, QColor +from PyQt6.QtCharts import QChart, QChartView, QPieSeries, QBarSeries, QBarSet, QBarCategoryAxis, QValueAxis + +class FitnessApp(QMainWindow): + def __init__(self): + super().__init__() + self.initDB() + self.initUI() + + def initDB(self): + """Инициализация базы данных""" + self.conn = sqlite3.connect('fitness.db') + self.cursor = self.conn.cursor() + + # Создание таблиц + self.create_tables() + # Заполнение тестовыми данными + self.insert_sample_data() + + def create_tables(self): + """Создание таблиц базы данных""" + tables = [ + """ + CREATE TABLE IF NOT EXISTS Users ( + userID INTEGER PRIMARY KEY, + fio TEXT NOT NULL, + phone TEXT, + email TEXT, + login TEXT UNIQUE, + password TEXT, + userType TEXT, + specialization TEXT, + birthDate DATE + ) + """, + """ + CREATE TABLE IF NOT EXISTS Memberships ( + membershipID INTEGER PRIMARY KEY, + clientID INTEGER, + membershipType TEXT, + startDate DATE, + endDate DATE, + visitsTotal INTEGER, + visitsUsed INTEGER, + zones TEXT, + membershipStatus TEXT, + cost REAL, + adminID INTEGER, + FOREIGN KEY (clientID) REFERENCES Users(userID), + FOREIGN KEY (adminID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS Visits ( + visitID INTEGER PRIMARY KEY, + clientID INTEGER, + visitDate DATE, + checkInTime TIME, + checkOutTime TIME, + zone TEXT, + membershipID INTEGER, + FOREIGN KEY (clientID) REFERENCES Users(userID), + FOREIGN KEY (membershipID) REFERENCES Memberships(membershipID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS GroupClasses ( + classID INTEGER PRIMARY KEY, + className TEXT, + trainerID INTEGER, + classDate DATE, + startTime TIME, + endTime TIME, + hall TEXT, + maxParticipants INTEGER, + enrolledParticipants INTEGER, + classStatus TEXT, + FOREIGN KEY (trainerID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS PersonalTraining ( + trainingID INTEGER PRIMARY KEY, + clientID INTEGER, + trainerID INTEGER, + trainingDate DATE, + startTime TIME, + endTime TIME, + exercises TEXT, + notes TEXT, + progressMetrics TEXT, + FOREIGN KEY (clientID) REFERENCES Users(userID), + FOREIGN KEY (trainerID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS ClassRegistrations ( + registrationID INTEGER PRIMARY KEY, + classID INTEGER, + clientID INTEGER, + registrationDate DATE, + status TEXT, + FOREIGN KEY (classID) REFERENCES GroupClasses(classID), + FOREIGN KEY (clientID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS EquipmentRequests ( + requestID INTEGER PRIMARY KEY, + trainerID INTEGER, + equipment TEXT, + quantity INTEGER, + requestDate DATE, + status TEXT, + FOREIGN KEY (trainerID) REFERENCES Users(userID) + ) + """, + """ + CREATE TABLE IF NOT EXISTS ShiftSwaps ( + swapID INTEGER PRIMARY KEY, + trainerID1 INTEGER, + trainerID2 INTEGER, + shiftDate DATE, + status TEXT, + FOREIGN KEY (trainerID1) REFERENCES Users(userID), + FOREIGN KEY (trainerID2) REFERENCES Users(userID) + ) + """ + ] + + for table in tables: + self.cursor.execute(table) + self.conn.commit() + + def insert_sample_data(self): + """Вставка тестовых данных""" + # Проверяем, есть ли уже данные + self.cursor.execute("SELECT COUNT(*) FROM Users") + if self.cursor.fetchone()[0] > 0: + return + + users = [ + (1, 'Сидорова Марина Петровна', '89219014567', 'director@fitness.ru', 'director1', 'pass1', 'Директор', '', '1980-05-15'), + (2, 'Романова Анна Сергеевна', '89210125678', 'admin1@fitness.ru', 'admin1', 'pass2', 'Администратор', '', '1992-08-22'), + (4, 'Яковлева Елена Викторовна', '89211236789', 'admin2@fitness.ru', 'admin2', 'pass3', 'Администратор', '', '1988-11-10'), + (7, 'Петров Дмитрий Александрович', '89212347890', 'petrov@fitness.ru', 'trainer1', 'pass4', 'Тренер', 'Силовые тренировки', '1985-03-18'), + (9, 'Смирнова Ольга Игоревна', '89213458901', 'smirnova@fitness.ru', 'trainer2', 'pass5', 'Тренер', 'Йога, Пилатес', '1990-07-25'), + (11, 'Козлов Сергей Николаевич', '89214569012', 'kozlov@fitness.ru', 'trainer3', 'pass6', 'Тренер', 'Плавание', '1987-12-05'), + (16, 'Федорова Екатерина Дмитриевна', '89161112236', 'fedorova@mail.ru', 'client1', 'pass7', 'Клиент', '', '1995-04-12'), + (21, 'Михайлов Алексей Владимирович', '89162223347', 'mikhailov@gmail.com', 'client2', 'pass8', 'Клиент', '', '1988-09-30'), + (26, 'Новикова Ирина Сергеевна', '89163334458', 'novikova@yandex.ru', 'client3', 'pass9', 'Клиент', '', '1992-06-18'), + (30, 'Соколов Игорь Петрович', '89164445569', 'sokolov@mail.ru', 'client4', 'pass10', 'Клиент', '', '1983-02-28'), + (34, 'Павлова Мария Александровна', '89165556670', 'pavlova@gmail.com', 'client5', 'pass11', 'Клиент', '', '1997-11-07') + ] + + memberships = [ + (1, 16, 'Месячный безлимит', '2024-06-01', '2024-06-30', 999, 42, 'Зал, Бассейн, Групповые', 'Активен', 5000.00, 2), + (2, 21, '12 посещений', '2024-06-05', '2024-09-05', 12, 8, 'Зал', 'Активен', 4000.00, 2), + (3, 26, 'Годовой VIP', '2024-01-10', '2025-01-10', 999, 156, 'Все зоны', 'Активен', 45000.00, 4), + (4, 30, 'Разовое посещение', '2024-06-15', '2024-06-15', 1, 1, 'Зал', 'Завершен', 500.00, 2), + (5, 34, 'Квартальный', '2024-06-01', '2024-08-31', 999, 15, 'Зал, Групповые', 'Активен', 12000.00, 4) + ] + + visits = [ + (1, 16, '2024-06-15', '08:30', '10:15', 'Тренажерный зал', 1), + (2, 21, '2024-06-15', '09:00', '10:30', 'Тренажерный зал', 2), + (3, 26, '2024-06-15', '07:00', '08:30', 'Бассейн', 3), + (4, 16, '2024-06-15', '18:00', '19:45', 'Групповое занятие', 1), + (5, 34, '2024-06-15', '19:00', '20:30', 'Групповое занятие', 5) + ] + + group_classes = [ + (1, 'Йога для начинающих', 9, '2024-06-16', '10:00', '11:00', 'Зал 2', 15, 12, 'Запланировано'), + (2, 'Силовая аэробика', 7, '2024-06-16', '18:00', '19:00', 'Зал 1', 20, 18, 'Запланировано'), + (3, 'Пилатес', 9, '2024-06-17', '11:00', '12:00', 'Зал 2', 12, 12, 'Группа заполнена'), + (4, 'Аквааэробика', 11, '2024-06-17', '15:00', '16:00', 'Бассейн', 10, 7, 'Запланировано'), + (5, 'Бокс', 7, '2024-06-18', '19:00', '20:30', 'Зал 3', 8, 5, 'Запланировано') + ] + + personal_training = [ + (1, 26, 7, '2024-06-14', '16:00', '17:00', 'Жим лежа, Приседания, Тяга блока', 'Хорошая техника', 'Жим 80кг x 8'), + (2, 16, 9, '2024-06-13', '10:00', '11:00', 'Асаны йоги, Растяжка', 'Улучшилась гибкость', ''), + (3, 21, 7, '2024-06-12', '14:00', '15:00', 'Становая тяга, Жим гантелей', 'Нужно работать над техникой', 'Становая 60кг x 6') + ] + + # Вставка данных + self.cursor.executemany("INSERT INTO Users VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", users) + self.cursor.executemany("INSERT INTO Memberships VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", memberships) + self.cursor.executemany("INSERT INTO Visits VALUES (?, ?, ?, ?, ?, ?, ?)", visits) + self.cursor.executemany("INSERT INTO GroupClasses VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", group_classes) + self.cursor.executemany("INSERT INTO PersonalTraining VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", personal_training) + self.conn.commit() + + def initUI(self): + """Инициализация пользовательского интерфейса""" + self.setWindowTitle('Фитнес-клуб - Система управления') + self.setGeometry(100, 100, 1200, 800) + + # Центральный виджет с вкладками для разных ролей + self.tabs = QTabWidget() + + # Вкладка для администратора + self.admin_tab = QWidget() + self.init_admin_tab() + self.tabs.addTab(self.admin_tab, "Администратор") + + # Вкладка для тренера + self.trainer_tab = QWidget() + self.init_trainer_tab() + self.tabs.addTab(self.trainer_tab, "Тренер") + + # Вкладка для директора + self.director_tab = QWidget() + self.init_director_tab() + self.tabs.addTab(self.director_tab, "Директор") + + self.setCentralWidget(self.tabs) + + def init_admin_tab(self): + """Инициализация вкладки администратора""" + layout = QVBoxLayout() + + # Группа управления расписанием + schedule_group = QGroupBox("Управление расписанием групповых занятий") + schedule_layout = QVBoxLayout() + + # Таблица групповых занятий + self.classes_table = QTableWidget() + self.classes_table.setColumnCount(10) + self.classes_table.setHorizontalHeaderLabels([ + 'ID', 'Название', 'Тренер', 'Дата', 'Время начала', + 'Время окончания', 'Зал', 'Макс. участников', 'Записано', 'Статус' + ]) + schedule_layout.addWidget(self.classes_table) + + # Кнопки управления + btn_layout = QHBoxLayout() + self.add_class_btn = QPushButton("Добавить занятие") + self.edit_class_btn = QPushButton("Редактировать") + self.delete_class_btn = QPushButton("Удалить") + + btn_layout.addWidget(self.add_class_btn) + btn_layout.addWidget(self.edit_class_btn) + btn_layout.addWidget(self.delete_class_btn) + + schedule_layout.addLayout(btn_layout) + schedule_group.setLayout(schedule_layout) + layout.addWidget(schedule_group) + + # Группа управления абонементами + membership_group = QGroupBox("Управление абонементами") + membership_layout = QVBoxLayout() + + self.memberships_table = QTableWidget() + self.memberships_table.setColumnCount(8) + self.memberships_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Тип', 'Начало', 'Окончание', + 'Использовано/Всего', 'Статус', 'Стоимость' + ]) + membership_layout.addWidget(self.memberships_table) + + # Кнопки для управления абонементами + membership_btn_layout = QHBoxLayout() + self.change_price_btn = QPushButton("Изменить стоимость") + self.freeze_membership_btn = QPushButton("Заморозить абонемент") + + membership_btn_layout.addWidget(self.change_price_btn) + membership_btn_layout.addWidget(self.freeze_membership_btn) + membership_layout.addLayout(membership_btn_layout) + + membership_group.setLayout(membership_layout) + layout.addWidget(membership_group) + + # Группа статистики + stats_group = QGroupBox("Статистика и отчетность") + stats_layout = QHBoxLayout() + + # Левая часть - форма для фильтров + stats_form = QFormLayout() + self.stats_start_date = QDateEdit() + self.stats_end_date = QDateEdit() + self.stats_start_date.setDate(QDate.currentDate().addMonths(-1)) + self.stats_end_date.setDate(QDate.currentDate()) + + stats_form.addRow("С:", self.stats_start_date) + stats_form.addRow("По:", self.stats_end_date) + + self.generate_stats_btn = QPushButton("Сформировать отчет") + stats_form.addRow(self.generate_stats_btn) + + stats_layout.addLayout(stats_form) + + # Правая часть - диаграмма + self.stats_chart = QChartView() + stats_layout.addWidget(self.stats_chart) + + stats_group.setLayout(stats_layout) + layout.addWidget(stats_group) + + # Загрузка данных + self.load_classes_data() + self.load_memberships_data() + + # Подключение сигналов + self.add_class_btn.clicked.connect(self.add_class) + self.generate_stats_btn.clicked.connect(self.generate_stats) + + self.admin_tab.setLayout(layout) + + def init_trainer_tab(self): + """Инициализация вкладки тренера""" + layout = QVBoxLayout() + + # Группа программ тренировок + programs_group = QGroupBox("Индивидуальные программы тренировок") + programs_layout = QVBoxLayout() + + self.programs_table = QTableWidget() + self.programs_table.setColumnCount(6) + self.programs_table.setHorizontalHeaderLabels([ + 'ID', 'Клиент', 'Дата', 'Упражнения', 'Заметки', 'Прогресс' + ]) + programs_layout.addWidget(self.programs_table) + + programs_btn_layout = QHBoxLayout() + self.add_program_btn = QPushButton("Добавить программу") + self.edit_program_btn = QPushButton("Редактировать") + + programs_btn_layout.addWidget(self.add_program_btn) + programs_btn_layout.addWidget(self.edit_program_btn) + programs_layout.addLayout(programs_btn_layout) + + programs_group.setLayout(programs_layout) + layout.addWidget(programs_group) + + # Группа базы упражнений + exercises_group = QGroupBox("База упражнений") + exercises_layout = QVBoxLayout() + + self.exercises_table = QTableWidget() + self.exercises_table.setColumnCount(3) + self.exercises_table.setHorizontalHeaderLabels(['ID', 'Название', 'Группа мышц']) + exercises_layout.addWidget(self.exercises_table) + + exercises_btn_layout = QHBoxLayout() + self.add_exercise_btn = QPushButton("Добавить упражнение") + exercises_btn_layout.addWidget(self.add_exercise_btn) + exercises_layout.addLayout(exercises_btn_layout) + + exercises_group.setLayout(exercises_layout) + layout.addWidget(exercises_group) + + # Группа запросов оборудования + equipment_group = QGroupBox("Запросы оборудования") + equipment_layout = QVBoxLayout() + + self.equipment_table = QTableWidget() + self.equipment_table.setColumnCount(5) + self.equipment_table.setHorizontalHeaderLabels([ + 'ID', 'Оборудование', 'Количество', 'Дата запроса', 'Статус' + ]) + equipment_layout.addWidget(self.equipment_table) + + equipment_btn_layout = QHBoxLayout() + self.request_equipment_btn = QPushButton("Запросить оборудование") + equipment_btn_layout.addWidget(self.request_equipment_btn) + equipment_layout.addLayout(equipment_btn_layout) + + equipment_group.setLayout(equipment_layout) + layout.addWidget(equipment_group) + + # Загрузка данных + self.load_programs_data() + self.load_equipment_data() + + self.trainer_tab.setLayout(layout) + + def init_director_tab(self): + """Инициализация вкладки директора""" + layout = QVBoxLayout() + + # Общая статистика + overall_stats_group = QGroupBox("Общая статистика клуба") + overall_layout = QHBoxLayout() + + # Левая часть - ключевые показатели + metrics_layout = QFormLayout() + self.total_members_label = QLabel("0") + self.active_members_label = QLabel("0") + self.monthly_revenue_label = QLabel("0 руб.") + self.attendance_rate_label = QLabel("0%") + + metrics_layout.addRow("Всего клиентов:", self.total_members_label) + metrics_layout.addRow("Активных абонементов:", self.active_members_label) + metrics_layout.addRow("Доход за месяц:", self.monthly_revenue_label) + metrics_layout.addRow("Посещаемость:", self.attendance_rate_label) + + overall_layout.addLayout(metrics_layout) + + # Правая часть - диаграмма доходов + self.revenue_chart = QChartView() + overall_layout.addWidget(self.revenue_chart) + + overall_stats_group.setLayout(overall_layout) + layout.addWidget(overall_stats_group) + + # Эффективность тренеров + trainers_group = QGroupBox("Эффективность тренеров") + trainers_layout = QVBoxLayout() + + self.trainers_table = QTableWidget() + self.trainers_table.setColumnCount(6) + self.trainers_table.setHorizontalHeaderLabels([ + 'Тренер', 'Групповые занятия', 'Персональные тренировки', + 'Средняя оценка', 'Доход', 'Бонусы' + ]) + trainers_layout.addWidget(self.trainers_table) + + trainers_group.setLayout(trainers_layout) + layout.addWidget(trainers_group) + + # Финансовые показатели + finance_group = QGroupBox("Финансовые показатели") + finance_layout = QVBoxLayout() + + self.finance_table = QTableWidget() + self.finance_table.setColumnCount(4) + self.finance_table.setHorizontalHeaderLabels([ + 'Период', 'Доход', 'Расходы', 'Прибыль' + ]) + finance_layout.addWidget(self.finance_table) + + finance_group.setLayout(finance_layout) + layout.addWidget(finance_group) + + # Загрузка данных + self.load_director_data() + + self.director_tab.setLayout(layout) + + def load_classes_data(self): + """Загрузка данных о групповых занятиях""" + self.cursor.execute(""" + SELECT gc.classID, gc.className, u.fio, gc.classDate, gc.startTime, + gc.endTime, gc.hall, gc.maxParticipants, gc.enrolledParticipants, gc.classStatus + FROM GroupClasses gc + LEFT JOIN Users u ON gc.trainerID = u.userID + ORDER BY gc.classDate, gc.startTime + """) + classes = self.cursor.fetchall() + + self.classes_table.setRowCount(len(classes)) + for row, class_data in enumerate(classes): + for col, data in enumerate(class_data): + self.classes_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_memberships_data(self): + """Загрузка данных об абонементах""" + self.cursor.execute(""" + SELECT m.membershipID, u.fio, m.membershipType, m.startDate, m.endDate, + m.visitsUsed || '/' || m.visitsTotal, m.membershipStatus, m.cost + FROM Memberships m + JOIN Users u ON m.clientID = u.userID + ORDER BY m.endDate + """) + memberships = self.cursor.fetchall() + + self.memberships_table.setRowCount(len(memberships)) + for row, membership_data in enumerate(memberships): + for col, data in enumerate(membership_data): + self.memberships_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_programs_data(self): + """Загрузка данных о программах тренировок""" + self.cursor.execute(""" + SELECT pt.trainingID, u.fio, pt.trainingDate, pt.exercises, pt.notes, pt.progressMetrics + FROM PersonalTraining pt + JOIN Users u ON pt.clientID = u.userID + ORDER BY pt.trainingDate DESC + """) + programs = self.cursor.fetchall() + + self.programs_table.setRowCount(len(programs)) + for row, program_data in enumerate(programs): + for col, data in enumerate(program_data): + self.programs_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_equipment_data(self): + """Загрузка данных о запросах оборудования""" + self.cursor.execute(""" + SELECT requestID, equipment, quantity, requestDate, status + FROM EquipmentRequests + ORDER BY requestDate DESC + """) + equipment = self.cursor.fetchall() + + self.equipment_table.setRowCount(len(equipment)) + for row, equipment_data in enumerate(equipment): + for col, data in enumerate(equipment_data): + self.equipment_table.setItem(row, col, QTableWidgetItem(str(data))) + + def load_director_data(self): + """Загрузка данных для директора""" + # Общая статистика + self.cursor.execute("SELECT COUNT(*) FROM Users WHERE userType = 'Клиент'") + total_clients = self.cursor.fetchone()[0] + self.total_members_label.setText(str(total_clients)) + + self.cursor.execute("SELECT COUNT(*) FROM Memberships WHERE membershipStatus = 'Активен'") + active_memberships = self.cursor.fetchone()[0] + self.active_members_label.setText(str(active_memberships)) + + self.cursor.execute(""" + SELECT SUM(cost) FROM Memberships + WHERE strftime('%Y-%m', startDate) = strftime('%Y-%m', 'now') + """) + monthly_revenue = self.cursor.fetchone()[0] or 0 + self.monthly_revenue_label.setText(f"{monthly_revenue:.2f} руб.") + + # Диаграмма доходов + self.cursor.execute(""" + SELECT membershipType, SUM(cost) + FROM Memberships + WHERE strftime('%Y', startDate) = strftime('%Y', 'now') + GROUP BY membershipType + """) + revenue_data = self.cursor.fetchall() + + series = QPieSeries() + for membership_type, revenue in revenue_data: + series.append(membership_type, revenue) + + chart = QChart() + chart.addSeries(series) + chart.setTitle("Доходы по типам абонементов") + chart.legend().setVisible(True) + chart.legend().setAlignment(Qt.AlignmentFlag.AlignBottom) + + self.revenue_chart.setChart(chart) + + # Данные по тренерам + self.cursor.execute(""" + SELECT u.fio, + COUNT(DISTINCT gc.classID) as group_classes, + COUNT(DISTINCT pt.trainingID) as personal_trainings, + '4.5' as avg_rating, + SUM(m.cost * 0.1) as revenue, + COUNT(DISTINCT pt.trainingID) * 100 as bonuses + FROM Users u + LEFT JOIN GroupClasses gc ON u.userID = gc.trainerID + LEFT JOIN PersonalTraining pt ON u.userID = pt.trainerID + LEFT JOIN Memberships m ON pt.clientID = m.clientID + WHERE u.userType = 'Тренер' + GROUP BY u.userID, u.fio + """) + trainers = self.cursor.fetchall() + + self.trainers_table.setRowCount(len(trainers)) + for row, trainer_data in enumerate(trainers): + for col, data in enumerate(trainer_data): + self.trainers_table.setItem(row, col, QTableWidgetItem(str(data))) + + def add_class(self): + """Добавление нового группового занятия""" + dialog = AddClassDialog(self) + if dialog.exec(): + class_data = dialog.get_data() + try: + self.cursor.execute(""" + INSERT INTO GroupClasses (className, trainerID, classDate, startTime, endTime, hall, maxParticipants, enrolledParticipants, classStatus) + VALUES (?, ?, ?, ?, ?, ?, ?, 0, 'Запланировано') + """, class_data) + self.conn.commit() + self.load_classes_data() + QMessageBox.information(self, "Успех", "Занятие успешно добавлено!") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось добавить занятие: {str(e)}") + + def generate_stats(self): + """Генерация статистики посещаемости""" + start_date = self.stats_start_date.date().toString("yyyy-MM-dd") + end_date = self.stats_end_date.date().toString("yyyy-MM-dd") + + self.cursor.execute(""" + SELECT zone, COUNT(*) as visits + FROM Visits + WHERE visitDate BETWEEN ? AND ? + GROUP BY zone + """, (start_date, end_date)) + zone_stats = self.cursor.fetchall() + + series = QBarSeries() + bar_set = QBarSet("Посещения по зонам") + + categories = [] + visits = [] + + for zone, count in zone_stats: + categories.append(zone) + visits.append(count) + + bar_set.append(visits) + series.append(bar_set) + + chart = QChart() + chart.addSeries(series) + chart.setTitle(f"Посещаемость по зонам ({start_date} - {end_date})") + + axis_x = QBarCategoryAxis() + axis_x.append(categories) + chart.addAxis(axis_x, Qt.AlignmentFlag.AlignBottom) + series.attachAxis(axis_x) + + axis_y = QValueAxis() + chart.addAxis(axis_y, Qt.AlignmentFlag.AlignLeft) + series.attachAxis(axis_y) + + self.stats_chart.setChart(chart) + +class AddClassDialog(QMessageBox): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Добавить групповое занятие") + self.setModal(True) + + self.class_name = QLineEdit() + self.trainer = QComboBox() + self.class_date = QDateEdit() + self.start_time = QTimeEdit() + self.end_time = QTimeEdit() + self.hall = QComboBox() + self.max_participants = QSpinBox() + + self.class_date.setDate(QDate.currentDate()) + self.start_time.setTime(QTime.currentTime()) + self.end_time.setTime(QTime.currentTime().addSecs(3600)) + self.max_participants.setRange(1, 100) + + # Заполнение комбобоксов + parent.cursor.execute("SELECT userID, fio FROM Users WHERE userType = 'Тренер'") + trainers = parent.cursor.fetchall() + for trainer_id, fio in trainers: + self.trainer.addItem(fio, trainer_id) + + self.hall.addItems(['Зал 1', 'Зал 2', 'Зал 3', 'Бассейн']) + + layout = QFormLayout() + layout.addRow("Название:", self.class_name) + layout.addRow("Тренер:", self.trainer) + layout.addRow("Дата:", self.class_date) + layout.addRow("Время начала:", self.start_time) + layout.addRow("Время окончания:", self.end_time) + layout.addRow("Зал:", self.hall) + layout.addRow("Макс. участников:", self.max_participants) + + widget = QWidget() + widget.setLayout(layout) + self.layout().addWidget(widget, 0, 0, 1, self.layout().columnCount()) + + self.addButton(QPushButton("Добавить"), QMessageBox.ButtonRole.AcceptRole) + self.addButton(QPushButton("Отмена"), QMessageBox.ButtonRole.RejectRole) + + def get_data(self): + """Получение данных из формы""" + return ( + self.class_name.text(), + self.trainer.currentData(), + self.class_date.date().toString("yyyy-MM-dd"), + self.start_time.time().toString("hh:mm"), + self.end_time.time().toString("hh:mm"), + self.hall.currentText(), + self.max_participants.value() + ) + +if __name__ == '__main__': + app = QApplication(sys.argv) + + # Установка стиля + app.setStyle('Fusion') + + window = FitnessApp() + window.show() + + sys.exit(app.exec()) diff --git a/fitness.db b/fitness.db index 8472fde..4222c95 100644 Binary files a/fitness.db and b/fitness.db differ diff --git a/masterpol.db b/masterpol.db index 12f3f3d..5cb6c5f 100644 Binary files a/masterpol.db and b/masterpol.db differ diff --git a/service_requests.db b/service_requests.db index 65c3422..2594dc9 100644 Binary files a/service_requests.db and b/service_requests.db differ diff --git a/service_requests_v2.db b/service_requests_v2.db new file mode 100644 index 0000000..977ae18 Binary files /dev/null and b/service_requests_v2.db differ