diff --git a/.idea/misc.xml b/.idea/misc.xml index e17b4fa..bef5a0a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,4 @@ - - - + \ No newline at end of file diff --git a/.idea/vision-macula.iml b/.idea/vision-macula.iml index 4ada2a6..62b7f51 100644 --- a/.idea/vision-macula.iml +++ b/.idea/vision-macula.iml @@ -7,7 +7,7 @@ - + \ No newline at end of file diff --git a/database.db b/database.db new file mode 100644 index 0000000..f749bb3 Binary files /dev/null and b/database.db differ diff --git a/database_schema.sql b/database_schema.sql new file mode 100644 index 0000000..dbe0a0c Binary files /dev/null and b/database_schema.sql differ diff --git a/src/bsmu/macula/app/2.png b/src/bsmu/macula/app/2.png new file mode 100644 index 0000000..834570c Binary files /dev/null and b/src/bsmu/macula/app/2.png differ diff --git a/src/bsmu/macula/app/database.db b/src/bsmu/macula/app/database.db new file mode 100644 index 0000000..7aaefac Binary files /dev/null and b/src/bsmu/macula/app/database.db differ diff --git a/src/bsmu/macula/app/edit.png b/src/bsmu/macula/app/edit.png new file mode 100644 index 0000000..6b44614 Binary files /dev/null and b/src/bsmu/macula/app/edit.png differ diff --git a/src/bsmu/macula/app/example.db b/src/bsmu/macula/app/example.db new file mode 100644 index 0000000..e69de29 diff --git a/src/bsmu/macula/app/images/icons/edit.png b/src/bsmu/macula/app/images/icons/edit.png new file mode 100644 index 0000000..6b44614 Binary files /dev/null and b/src/bsmu/macula/app/images/icons/edit.png differ diff --git a/src/bsmu/macula/configs/default/bsmu.macula/app.MaculaApp.conf.yaml b/src/bsmu/macula/configs/default/bsmu.macula/app.MaculaApp.conf.yaml index 56a0c75..d3e9922 100644 --- a/src/bsmu/macula/configs/default/bsmu.macula/app.MaculaApp.conf.yaml +++ b/src/bsmu/macula/configs/default/bsmu.macula/app.MaculaApp.conf.yaml @@ -28,4 +28,6 @@ plugins: - bsmu.vision.plugins.task_storage_view.TaskStorageViewPlugin + - bsmu.macula.plugins.db.BD.BD - bsmu.macula.plugins.gui.ensemble_segmenter_gui.EnsembleSegmenterGuiPlugin + - bsmu.macula.plugins.db.database_manager.DatabaseManager diff --git a/src/bsmu/macula/configs/default/bsmu.macula/plugins.BD.conf.yaml b/src/bsmu/macula/configs/default/bsmu.macula/plugins.BD.conf.yaml new file mode 100644 index 0000000..e69de29 diff --git a/src/bsmu/macula/configs/default/bsmu.macula/plugins.database_manager.conf.yaml b/src/bsmu/macula/configs/default/bsmu.macula/plugins.database_manager.conf.yaml new file mode 100644 index 0000000..e69de29 diff --git a/src/bsmu/macula/plugins/db/BD.py b/src/bsmu/macula/plugins/db/BD.py new file mode 100644 index 0000000..cec7873 --- /dev/null +++ b/src/bsmu/macula/plugins/db/BD.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from PySide6.QtCore import Qt +from bsmu.vision.core.plugins import Plugin +from bsmu.vision.plugins.doc_interfaces.mdi import MdiPlugin +from bsmu.vision.plugins.windows.main import AlgorithmsMenu, MainWindowPlugin, MainWindow + +from bsmu.macula.plugins.db.SQLiteTableViewer import TableWidgetExample +from bsmu.macula.plugins.ensemble_segmenter import BinaryEnsemblePlugin + +if TYPE_CHECKING: + pass +class BD(Plugin): + _DEFAULT_DEPENDENCY_PLUGIN_FULL_NAME_BY_KEY = { + 'main_window_plugin': 'bsmu.vision.plugins.windows.main.MainWindowPlugin', + 'mdi_plugin': 'bsmu.vision.plugins.doc_interfaces.mdi.MdiPlugin' + } + + def __init__( + self, + main_window_plugin: MainWindowPlugin, + mdi_plugin: MdiPlugin + ): + super().__init__() + self._main_window_plugin = main_window_plugin + self._mdi_plugin = mdi_plugin + + self._ensemble_segmenter_gui: BinaryEnsemblePlugin | None = None + self._main_window: MainWindow | None = None + + @property + def ensemble_segmenter_gui(self) -> BinaryEnsemblePlugin | None: + return self._ensemble_segmenter_gui + + def _enable_gui(self): + self._main_window = self._main_window_plugin.main_window + + self._main_window.add_menu_action( + AlgorithmsMenu, + self.tr('BD'), + self._re + ) + def _re(self): + self.window = TableWidgetExample() + self.window.setWindowModality(Qt.ApplicationModal) + self.window.show() + + def _disable(self): + self._ensemble_segmenter_gui = None + self._main_window = None \ No newline at end of file diff --git a/src/bsmu/macula/plugins/db/SQLiteTableViewer.py b/src/bsmu/macula/plugins/db/SQLiteTableViewer.py new file mode 100644 index 0000000..3eed016 --- /dev/null +++ b/src/bsmu/macula/plugins/db/SQLiteTableViewer.py @@ -0,0 +1,494 @@ +from functools import partial + +from PySide6.QtGui import QIcon, Qt, QPixmap +from PySide6.QtSql import QSqlQueryModel, QSqlDatabase, QSqlQuery +from PySide6.QtWidgets import ( + QWidget, QTableWidget, QPushButton, QVBoxLayout, QHBoxLayout, QFrame, QLineEdit, QLabel, QScrollArea, QGridLayout, + QTableView, + QStyledItemDelegate, QMessageBox, QGroupBox, QFormLayout, QTabWidget, QDialog +) + +from bsmu.macula.plugins.db.add_patient_dialog import AddRecordDialog +from bsmu.macula.plugins.db.edit_pacirnt_delegate import EditPacientDelegate +from bsmu.macula.widgets.eye_data_widget import EyeDataWidget +from bsmu.macula.records.eye_info_data import PatientExamData + + +class TableWidgetExample(QWidget): + + def __init__(self): + super().__init__() + self.init_db() + self.createMainTables() + self.setWindowTitle("Пациенты") + self.resize(1200, 800) + self.appointments_table_created = False + self.appointment_data_table_created = False + + def init_db(self): + db = QSqlDatabase.addDatabase("QSQLITE") + db.setDatabaseName("database.db") + if not db.open(): + print("Ошибка подключения к базе данных") + return None + self.db = db + + def patient_clicked(self, index): + row = index.row() + user_id = self.patients_model.index(row, 0).data() + self.load_appointments_from_db(user_id) + + def apointment_clicked(self, index): + row = index.row() + apointment_id = self.appointments_model.index(row, 0).data() + self.load_appointment_data_from_db(apointment_id) + + self.init_ui() + # self.main_lay.addLayout(self.init_ui()) + + def createMainTables(self): + self.setWindowTitle("Пациенты") + self.resize(1200, 800) + + self.main_lay = QHBoxLayout(self) + left_layout = QVBoxLayout(self) + + db = QSqlDatabase.addDatabase("QSQLITE") + db.setDatabaseName("database.db") + if not db.open(): + print("Ошибка подключения к базе данных") + return None + + self.patients_model = QSqlQueryModel() + self.patients_model.setQuery("SELECT id, name, sex, year_of_birthday FROM pacients") + + dict_patient = { + # "id": "Ид", + "name": "Имя", + "sex": "Пол", + "year_of_birthday": "Год рождения", + "acts": "Действия" + } + + # for i in range(0, self.patients_model.columnCount()): + # self.patients_model.setHeaderData(i, Qt.Orientation.Horizontal, dict_patient[self.patients_model.headerData(i, Orientation.Horizontal )]) + + self.patients_table = QTableView(self) + self.patients_table.setModel(self.patients_model) + + self.patients_model.insertColumn(self.patients_model.columnCount()) + + self.patients_model.setHeaderData(0, Qt.Orientation.Horizontal, "Ид") + self.patients_model.setHeaderData(1, Qt.Orientation.Horizontal, "Имя") + self.patients_model.setHeaderData(2, Qt.Orientation.Horizontal, "Пол") + self.patients_model.setHeaderData(3, Qt.Orientation.Horizontal, "Год рождения") + self.patients_model.setHeaderData(4, Qt.Orientation.Horizontal, "Редактировать") + + self.patients_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + self.patients_table.clicked.connect(self.patient_clicked) + + self.appointment_edit_delegate = EditPacientDelegate(r"edit.png", self.patients_table, self.open_dialog) + self.patients_table.setItemDelegateForColumn(self.patients_model.columnCount() - 1, self.appointment_edit_delegate) + + + self.patients_table.setColumnHidden(0, True) + + add_patient = QPushButton("Добавить пациента") + add_patient.clicked.connect(partial(self.open_dialog, 0)) + button_layout = QHBoxLayout() + button_layout.addWidget(add_patient) + + self.appointments_table = QTableView(self) + add_appointment_button = QPushButton("Добавить результаты приема") + button_layout2 = QHBoxLayout() + button_layout2.addWidget(add_appointment_button) + + left_layout.addWidget(self.patients_table) + left_layout.addLayout(button_layout) + left_layout.addWidget(self.appointments_table) + left_layout.addLayout(button_layout2) + self.main_lay.addLayout(left_layout) + layout = QVBoxLayout() + self.tab_widget = QTabWidget() + layout.addWidget(self.tab_widget) + self.main_lay.addLayout(layout) + # layout2 = QVBoxLayout() + # + # # QLabel для превью + # self.preview_label = QLabel("Нажмите на изображение для увеличения") + # pixmap = QPixmap(rf"C:\Users\Evgeniy\OneDrive\Pictures\32022.jpg") # Подставьте путь к своему изображению + # self.preview_label.setPixmap(pixmap.scaled(200, 150)) # Масштабируем превью + # layout2.addWidget(self.preview_label) + # + # # Добавляем обработчик клика + # self.preview_label.mousePressEvent = self.open_full_image + # self.main_lay.addLayout(layout2) + # main_lay.addLayout(self.addlay()) + + def open_full_image(self, event): + """Открывает изображение в отдельном окне.""" + self.dialog = PreviewWindow(rf"C:\Users\Evgeniy\OneDrive\Pictures\32022.jpg") # Указываем путь к изображению + self.dialog.exec() + + def init_ui(self): + if (self.tab_widget.count() > 0) : + self.tab_widget.removeTab(0) + self.tab_widget.removeTab(0) + self.tab_widget.addTab(self.create_scrollable_area("Правый глаз", 0), "Правый глаз") + self.tab_widget.addTab(self.create_scrollable_area("Левый глаз", 1), "Левый глаз") + eye_data = PatientExamData.from_query_model(self.appointment_data_model, 1) + eye_widget = EyeDataWidget(eye_data) + self.tab_widget.addTab(eye_widget, "Левый глаз") + + + + def create_scrollable_area(self, layout_name, index_eye): + """Создаёт область прокрутки с блоками""" + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + + # Виджет-контейнер для полей + scroll_content = QWidget() + scroll_layout = QVBoxLayout() + + # Добавляем блоки в прокручиваемую область + scroll_layout.addWidget(self.create_block("Обследование", [ + ("Дата посещения", self.get_app_data_in(index_eye, 3)), + ("Продолжительность заболевания", self.get_app_data_in(index_eye, 4)), + ("Тип томографа ","Топкон"), + ("Критерий AREDS", self.get_app_data_in(index_eye, 7)), + ("Рефракция", self.get_app_data_in(index_eye, 8)), + ("Тип неоваскуляризации", self.get_app_data_in(index_eye, 9)) + ])) + scroll_layout.addWidget(self.create_block("Ретинальные показатели", [ + ("Толщина хориоидеи в центре", self.get_app_data_in(index_eye, 10)), + ("Толщина сетчатки в фовеоле", self.get_app_data_in(index_eye, 11)), + ("Общий объем", self.get_app_data_in(index_eye, 14)), + ("Средний объем", self.get_app_data_in(index_eye, 15)) + ])) + scroll_layout.addWidget(self.create_block("", [ + ("Состояние РПЭ", self.get_app_data_in(index_eye, 16)), + ("Локализация дефектов РПЭ", self.get_app_data_in(index_eye, 17)), + ("Локализация кистозного макулярного отека", self.get_app_data_in(index_eye, 18)) + ])) + scroll_layout.addWidget(QLabel("Отслойки РПЭ")) + scroll_layout.addWidget(self.create_block("Серозная ОПЭ", [ + ("Локализация", self.get_app_data_in(index_eye, 19)), + ("Ширина", self.get_app_data_in(index_eye, 20)), + ("Высота", self.get_app_data_in(index_eye, 21)), + ("Площадь", self.get_app_data_in(index_eye, 22)) + ])) + scroll_layout.addWidget(self.create_block("Геморрагическая ОПЭ", [ + ("Локализация", self.get_app_data_in(index_eye, 23)), + ("Ширина", self.get_app_data_in(index_eye, 24)), + ("Высота", self.get_app_data_in(index_eye, 25)), + ("Площадь", self.get_app_data_in(index_eye, 26)) + ])) + scroll_layout.addWidget(self.create_block("Фиброваскулярная ОПЭ", [ + ("Локализация", self.get_app_data_in(index_eye, 27)), + ("Ширина", self.get_app_data_in(index_eye, 28)), + ("Высота", self.get_app_data_in(index_eye, 29)), + ("Площадь", self.get_app_data_in(index_eye, 30)) + ])) + scroll_layout.addWidget(self.create_block("Друзеноидная ОПЭ", [ + ("Локализация", self.get_app_data_in(index_eye, 31)), + ("Ширина", self.get_app_data_in(index_eye, 32)), + ("Высота", self.get_app_data_in(index_eye, 33)), + ("Площадь", self.get_app_data_in(index_eye, 34)) + ])) + scroll_layout.addWidget(self.create_block("Друзы", [ + ("Локализация", self.get_app_data_in(index_eye, 35)), + ("Ширина", self.get_app_data_in(index_eye, 36)), + ("Высота", self.get_app_data_in(index_eye, 37)), + ("Площадь", self.get_app_data_in(index_eye, 38)) + ])) + scroll_layout.addWidget(self.create_block("Жидкость под РПЭ", [ + ("Пощадь", self.get_app_data_in(index_eye, 39)), + ("Локализация", self.get_app_data_in(index_eye, 40)) + ])) + scroll_layout.addWidget(self.create_block("Эллипсоидная зона", [ + ("Состояние", self.get_app_data_in(index_eye, 41)), + ("Локализация дефектов", self.get_app_data_in(index_eye, 42)) + ])) + scroll_layout.addWidget(self.create_block("Миоидная зона", [ + ("Состояние", self.get_app_data_in(index_eye, 43)), + ("Локализация дефектов", self.get_app_data_in(index_eye, 44)) + ])) + scroll_layout.addWidget(self.create_block("Отслойка нейросенсорной сетчатки", [ + ("Локализация", self.get_app_data_in(index_eye, 45)), + ("Ширина", self.get_app_data_in(index_eye, 46)), + ("Высота", self.get_app_data_in(index_eye, 47)), + ("Площадь", self.get_app_data_in(index_eye, 48)) + ])) + scroll_layout.addWidget(self.create_block("Гиперрефлективный материал", [ + ("Локализация", self.get_app_data_in(index_eye, 49)), + ("Площадь", self.get_app_data_in(index_eye, 50)) + ])) + + # Устанавливаем макет для контейнера + scroll_content.setLayout(scroll_layout) + scroll_area.setWidget(scroll_content) + + return scroll_area + + def get_app_data_in(self, x, y): + return str(self.appointment_data_model.index(x, y).data()) + def create_block(self, title, fields): + """Создание тематического блока с полями и значениями""" + group_box = QGroupBox(title) + form_layout = QFormLayout() + + # Добавляем поля с заранее заданными значениями + for field, value in fields: + input_field = QLineEdit() + input_field.setText(value) # Подстановка значения + form_layout.addRow(QLabel(field + ":"), input_field) + + group_box.setLayout(form_layout) + return group_box + + def open_dialog(self, patient_id): + self.dialog = AddRecordDialog(self.db, patient_id) + self.dialog.exec_() # Запуск модального окна + + def open_dialog_appointmernt(self, patient_id): + self.dialog = AddRecordDialog(self.db, patient_id) + self.dialog.exec_() # Запуск модального окна + + def load_appointments_from_db(self, pacientId): + db = self.open_connection() + if db is None: + return # Завершаем, если подключение не удалось + + self.appointments_model = QSqlQueryModel() + query = QSqlQuery() + query.prepare("SELECT * FROM appointments WHERE pacient_id = :pacient_id") + query.bindValue(":pacient_id", pacientId) + + if query.exec(): + self.appointments_model.setQuery(query) + else: + print("Ошибка выполнения запроса:", query.lastError().text()) + + table1Description = ["Ид", "Дата приема", "duration_of_the_disease", "Действия"] + + + # for col, field in enumerate(table1Description.keys()): + # self.patients_model.setHeaderData(col, Qt.Orientation.Horizontal, table1Description[field]) + + self.appointments_table.setModel(self.appointments_model) + + self.appointments_model.insertColumn(self.appointments_model.columnCount()) + + + self.appointments_model.setHeaderData(0, Qt.Orientation.Horizontal, "Ид") + self.appointments_model.setHeaderData(1, Qt.Orientation.Horizontal, "Дата приема") + self.appointments_model.setHeaderData(2, Qt.Orientation.Horizontal, "Длит. болезни") + # self.appointments_model.setHeaderData(3, Qt.Orientation.Horizontal, "Ид пациента") + self.appointments_model.setHeaderData(3, Qt.Orientation.Horizontal, "Редактировать") + self.appointments_model.setHeaderData(4, Qt.Orientation.Horizontal, "Показать") + + self.appointments_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + self.appointments_table.clicked.connect(self.apointment_clicked) + + self.appointment_edit_delegate2 = EditPacientDelegate(r"edit.png", self.patients_table, self.open_dialog_appointmernt) + self.appointments_table.setItemDelegateForColumn(self.patients_model.columnCount() - 2, self.appointment_edit_delegate2) + + self.appointment_edit_delegate3 = EditPacientDelegate(r"2.png", self.patients_table, self.open_dialog_appointmernt) + self.appointments_table.setItemDelegateForColumn(self.patients_model.columnCount() - 1, self.appointment_edit_delegate3) + + self.appointments_table.setColumnHidden(0, True) + + def load_appointment_data_from_db(self, appiontmentId): + self.appointment_data_model = QSqlQueryModel() + query = QSqlQuery() + query.prepare("SELECT * FROM eyes WHERE appointment_id = :appiontmentId") + query.bindValue(":appiontmentId", appiontmentId) + + if query.exec(): + self.appointment_data_model.setQuery(query) + else: + print("Ошибка выполнения запроса:", query.lastError().text()) + + print(self.appointment_data_model.index(0, 0).data()) + + def addlay(self): + fields = [ + "id", "eye", "appointment_id", "date", "duration_of_the_disease", + "topkon", "optopol", "areds", "refraction", "type_of_neovascularization", + "choroidal_thickness_center", "cts_foveola", "cts_sup_inner_fovea", + "cts_sup_out_fovea", "total_volume", "average_volume", "rpe_status", + "rpe_localisation", "cme_localisation", "serouz_rpe_detachment_localisation", + "serouz_rpe_detachment_width", "serouz_rpe_detachment_height", "serouz_rpe_detachment_area", + "hemorrhagic_rpe_detachment_localisation", "hemorrhagic_rpe_detachment_width", + "hemorrhagic_rpe_detachment_heidgt", "hemorrhagic_rpe_detachment_area", + "fibrovascular_rpe_detachment_localisation", "fibrovascular_rpe_detachment_width", + "fibrovascular_rpe_detachment_heidgt", "fibrovascular_rpe_detachment_area", + "drusenoid_detachment_rpe_localisation", "drusenoid_detachment_rpe_width", + "drusenoid_detachment_rpe_height", "drusenoid_detachment_rpe_area", + "druses_localisation", "druses_weigt", "druses_heigt", "dzuses_area", + "fluid_under_rpe_area", "fluid_under_rpe_localisation", "ez_status", + "ez_localisation", "myoidnz_status", "myoidnz_localisation", + "rne_detachment_localisation", "rne_detachment_width", "rne_detachment_heigt", + "rne_detachment_area", "hyperreflective_material_localisation", "hyperreflective_material_area" + ] + + layout = QVBoxLayout() + + # Прокрутка для длинных форм + scroll_area = QScrollArea() + scroll_widget = QWidget() + grid_layout = QGridLayout() + + # Динамическое добавление всех полей + row = 0 + input_fields = {} + for field in fields: + # Добавляем метку для каждого поля + label = QLabel(field.replace('_', ' ').capitalize()) + grid_layout.addWidget(label, row, 0) # Первый столбец + + # Добавляем редактируемое поле + input_field = QLineEdit() + input_fields[field] = input_field + grid_layout.addWidget(input_field, row, 1) # Второй столбец + + row += 1 + + # Добавляем разделительную линию + separator = QFrame() + separator.setFrameShape(QFrame.Shape.HLine) + separator.setFrameShadow(QFrame.Shadow.Sunken) + grid_layout.addWidget(separator, row, 0, 1, 2) + + row += 1 + + # Кнопка сохранения + save_button = QPushButton("Сохранить") + grid_layout.addWidget(save_button, row, 0, 1, 2) # На всю ширину + + scroll_widget.setLayout(grid_layout) + scroll_area.setWidget(scroll_widget) + scroll_area.setWidgetResizable(True) + + layout.addWidget(scroll_area) + return layout + + + def open_connection(self): + db = QSqlDatabase.addDatabase("QSQLITE") # Указываем тип базы данных (SQLite в данном случае) + db.setDatabaseName("database.db") # Указываем имя или путь к базе данных + + if not db.open(): # Проверяем успешность открытия + print("Ошибка подключения к базе данных!") + return None + else: + print("Соединение с базой данных установлено.") + return db + + +class ButtonDelegate(QStyledItemDelegate): + def paint(self, painter, option, index): + """Рисуем кнопку в ячейке""" + icon_path = r"/bsmu/macula/app/images/icons/edit.png" + icon = QIcon(icon_path) # 🔹 Здесь нужна иконка (например, карандаш) + icon.paint(painter, option.rect) + + def editorEvent(self, event, model, option, index): + """Обрабатываем нажатие на кнопку""" + if event.type() == event.Type.MouseButtonPress: + QMessageBox.information(option.widget, "Редактирование", f"Редактируем строку {index.row()}") + return True + + # def init_ui(self): + # # Основной виджет + # layout = QVBoxLayout() + # + # # Добавляем блоки с прокруткой + # layout.addWidget(self.create_scrollable_area()) + # + # # Устанавливаем главный макет + # return layout + # + # def create_scrollable_area(self): + # """Создаёт область прокрутки с блоками""" + # scroll_area = QScrollArea() + # scroll_area.setWidgetResizable(True) + # + # # Виджет-контейнер для полей + # scroll_content = QWidget() + # scroll_layout = QVBoxLayout() + # + # # Добавляем блоки в прокручиваемую область + # scroll_layout.addWidget(self.create_block("Ид и основное", [ + # "Ид", "Глаз", "Ид посещения", "Дата посещения", "Продолжительность заболевания" + # ])) + # scroll_layout.addWidget(self.create_block("Обследования", [ + # "топкон", "оптопол", "критерий AREDS", "рефракция", "тип неоваскуляризации" + # ])) + # scroll_layout.addWidget(self.create_block("Хороидальная толщина", [ + # "толщина хориоидеи в центре", "толщина ЦТС", "толщина ЦТС верхнего внутреннего слоя ямки", + # "толщина ЦТС верхнего наружного слоя ямки", "общий объем", "средний объем" + # ])) + # scroll_layout.addWidget(self.create_block("Состояние ЭПС", [ + # "состояние ЭПС", "локализация ЭПС", "локализация ЦМЭ", + # "локализация серозного отслоения ЭПС", "ширина серозного отслоения ЭПС", + # "высота серозного отслоения ЭПС", "площадь серозного отслоения ЭПС" + # ])) + # scroll_layout.addWidget(self.create_block("Геморрагическое ЭПС", [ + # "локализация геморрагического отслоения ЭПС", "ширина геморрагического отслоения ЭПС", + # "высота геморрагического отслоения ЭПС", "площадь геморрагического отслоения ЭПС" + # ])) + # scroll_layout.addWidget(self.create_block("Фиброваскулярное ЭПС", [ + # "локализация фиброваскулярного отслоения ЭПС", "ширина фиброваскулярного отслоения ЭПС", + # "высота фиброваскулярного отслоения ЭПС", "площадь фиброваскулярного отслоения ЭПС" + # ])) + # scroll_layout.addWidget(self.create_block("Друзы и ЭПС", [ + # "локализация друзеноидного отслоения ЭПС", "ширина друзеноидного отслоения ЭПС", + # "высота друзеноидного отслоения ЭПС", "площадь друзеноидного отслоения ЭПС", + # "локализация друз", "вес друз", "высота друз", "площадь друз" + # ])) + # scroll_layout.addWidget(self.create_block("Жидкость", [ + # "площадь жидкости под ЭПС", "локализация жидкости под ЭПС" + # ])) + # scroll_layout.addWidget(self.create_block("зоны Эллера и прочее", [ + # "состояние EZ", "локализация EZ", "состояние миоидной зоны", "локализация миоидной зоны" + # ])) + # scroll_layout.addWidget(self.create_block("сетчатки и гиперрефлективный материал", [ + # "локализация отслоения РНЭ", "ширина отслоения РНЭ", "высота отслоения РНЭ", + # "площадь отслоения РНЭ", "локализация гиперотражающего материала", + # "площадь гиперотражающего материала" + # ])) + # + # # Устанавливаем макет для виджета + # scroll_content.setLayout(scroll_layout) + # scroll_area.setWidget(scroll_content) + # + # return scroll_area + # + # def create_block(self, title, fields): + # """Создание тематического блока с полями""" + # group_box = QGroupBox(title) + # form_layout = QFormLayout() + # + # # Добавляем поля + # for field in fields: + # form_layout.addRow(QLabel(field + ":"), QLineEdit()) + # + # group_box.setLayout(form_layout) + # return group_box +class PreviewWindow(QDialog): + """Окно для отображения полного изображения.""" + def __init__(self, image_path): + super().__init__() + self.setWindowTitle("Полное изображение") + self.resize(800, 600) + + layout = QVBoxLayout(self) + + # QLabel для изображения + full_image_label = QLabel(self) + pixmap = QPixmap(image_path) + full_image_label.setPixmap(pixmap) + full_image_label.setScaledContents(True) # Масштабируем изображение + layout.addWidget(full_image_label) \ No newline at end of file diff --git a/src/bsmu/macula/plugins/db/__init__.py b/src/bsmu/macula/plugins/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bsmu/macula/plugins/db/add_appointment_dialog.py b/src/bsmu/macula/plugins/db/add_appointment_dialog.py new file mode 100644 index 0000000..22449aa --- /dev/null +++ b/src/bsmu/macula/plugins/db/add_appointment_dialog.py @@ -0,0 +1,72 @@ +from functools import partial + +from PySide6.QtSql import QSqlQuery +from PySide6.QtWidgets import QDialog, QLineEdit, QPushButton, QVBoxLayout, QLabel + +from bsmu.macula.plugins.db.database_manager import DatabaseManager + + +class AddApponitmentDialog(QDialog): + def __init__(self, appotintment_id): + super().__init__() + self.db_manager = DatabaseManager() + self.is_new_appointment = appotintment_id == 0 + self.setWindowTitle("Добавить прием" if self.is_new_appointment else "Изменить данные приема") + + self.name_input = QLineEdit() + self.sex_input = QLineEdit() + self.age_input = QLineEdit() + self.save_button = QPushButton("Сохранить" if self.is_new_appointment else "Редактировать") + if (self.is_new_appointment): + self.save_button.clicked.connect(self.save_data) + else: + self.save_button.clicked.connect(partial(self.edit_data, appotintment_id)) + + layout = QVBoxLayout() + layout.addWidget(QLabel("Имя:")) + layout.addWidget(self.name_input) + layout.addWidget(QLabel("Пол:")) + layout.addWidget(self.sex_input) + layout.addWidget(QLabel("Год рождения:")) + layout.addWidget(self.age_input) + layout.addWidget(self.save_button) + + if (not self.is_new_appointment): + record = self.db_manager.fetch_record_by_id("pacients", appotintment_id) + # query = QSqlQuery(self.db) + # query.prepare("SELECT id, name, sex, year_of_birthday FROM pacients WHERE id = :pacient_id") + # query.bindValue(":pacient_id", patient_id) + # query.exec_() + # self.appointments_model = QSqlQueryModel() + # self.appointments_model.setQuery(query) + self.name_input.setText(record[0][1]) + self.sex_input.setText(record[0][2]) + self.age_input.setText(str(record[0][3])) + self.setLayout(layout) + + def save_data(self): + name = self.name_input.text() + sex = self.sex_input.text() + age = self.age_input.text() + + if name and age.isdigit(): + query = QSqlQuery(self.db) + query.prepare("INSERT INTO pacients (name, sex, year_of_birthday) VALUES (?, ?, ?)") + query.addBindValue(name) + query.addBindValue(sex) + query.addBindValue(int(age)) + query.exec_() + self.close() + else: + print("Ошибка: введите корректные данные") + + def edit_data(self, patient_id): + name = self.name_input.text() + sex = self.sex_input.text() + age = self.age_input.text() + + if name and sex and age.isdigit(): + self.db_manager.update_pacient(name, sex, int(age), patient_id) + self.close() + else: + print("Ошибка: введите корректные данные") \ No newline at end of file diff --git a/src/bsmu/macula/plugins/db/add_patient_dialog.py b/src/bsmu/macula/plugins/db/add_patient_dialog.py new file mode 100644 index 0000000..c18b29b --- /dev/null +++ b/src/bsmu/macula/plugins/db/add_patient_dialog.py @@ -0,0 +1,80 @@ +from functools import partial + +from PySide6.QtSql import QSqlQuery +from PySide6.QtWidgets import QDialog, QLineEdit, QPushButton, QVBoxLayout, QLabel + +from bsmu.macula.plugins.db.database_manager import DatabaseManager + + +class AddRecordDialog(QDialog): + def __init__(self, db, patient_id): + super().__init__() + self.db = db + self.db_manager = DatabaseManager() + self.is_new_user = patient_id == 0 + self.setWindowTitle("Добавить пациента" if self.is_new_user else "Изменить данные пациента") + + self.name_input = QLineEdit() + self.sex_input = QLineEdit() + self.age_input = QLineEdit() + self.save_button = QPushButton("Сохранить" if self.is_new_user else "Редактировать") + if (self.is_new_user): + self.save_button.clicked.connect(self.save_data) + else: + self.save_button.clicked.connect(partial(self.edit_data, patient_id)) + + layout = QVBoxLayout() + layout.addWidget(QLabel("Имя:")) + layout.addWidget(self.name_input) + layout.addWidget(QLabel("Пол:")) + layout.addWidget(self.sex_input) + layout.addWidget(QLabel("Год рождения:")) + layout.addWidget(self.age_input) + layout.addWidget(self.save_button) + + if (not self.is_new_user): + record = self.db_manager.fetch_record_by_id("pacients", patient_id) + # query = QSqlQuery(self.db) + # query.prepare("SELECT id, name, sex, year_of_birthday FROM pacients WHERE id = :pacient_id") + # query.bindValue(":pacient_id", patient_id) + # query.exec_() + # self.appointments_model = QSqlQueryModel() + # self.appointments_model.setQuery(query) + self.name_input.setText(record[0][1]) + self.sex_input.setText(record[0][2]) + self.age_input.setText(str(record[0][3])) + self.setLayout(layout) + + def save_data(self): + name = self.name_input.text() + sex = self.sex_input.text() + age = self.age_input.text() + + if name and age.isdigit(): + query = QSqlQuery(self.db) + query.prepare("INSERT INTO pacients (name, sex, year_of_birthday) VALUES (?, ?, ?)") + query.addBindValue(name) + query.addBindValue(sex) + query.addBindValue(int(age)) + query.exec_() + self.close() + else: + print("Ошибка: введите корректные данные") + + def edit_data(self, patient_id): + name = self.name_input.text() + sex = self.sex_input.text() + age = self.age_input.text() + + if name and sex and age.isdigit(): + self.db_manager.update_pacient(name, sex, int(age), patient_id) + # query = QSqlQuery(self.db) + # query.prepare("UPDATE pacients SET name = ? WHERE id = ?") + # query.addBindValue(name) + # query.addBindValue(sex) + # query.addBindValue(int(age)) + # query.addBindValue(patient_id) + # bol = query.exec_() + self.close() + else: + print("Ошибка: введите корректные данные") \ No newline at end of file diff --git a/src/bsmu/macula/plugins/db/database_manager.py b/src/bsmu/macula/plugins/db/database_manager.py new file mode 100644 index 0000000..196b730 --- /dev/null +++ b/src/bsmu/macula/plugins/db/database_manager.py @@ -0,0 +1,415 @@ +import sqlite3 + +from PySide6.QtSql import QSqlQuery, QSqlDatabase +from bsmu.vision.core.plugins import Plugin + + +class DatabaseManager(Plugin): + CREATE_PATIENTS = '''CREATE TABLE IF NOT EXISTS pacients ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + sex VARCHAR(1) NOT NULL, + year_of_birthday INTEGER NOT NULL +)''' + + CREATE_APPOINTMENTS = '''CREATE TABLE IF NOT EXISTS appointments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date DATE NOT NULL, + duration_of_the_disease INTEGER NOT NULL, + pacient_id INTEGER NOT NULL, + FOREIGN KEY (pacient_id) REFERENCES pacients (id) +)''' + + CREATE_EYES = '''CREATE TABLE IF NOT EXISTS eyes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + eye VARCHAR(2) NOT NULL, + appointment_id INTEGER NOT NULL, + date DATE NOT NULL, + duration_of_the_disease INTEGER NOT NULL, + topkon BOOLEAN NOT NULL, + optopol BOOLEAN NOT NULL, + areds VARCHAR(10) NOT NULL, + refraction VARCHAR(10) NOT NULL, + type_of_neovascularization VARCHAR(10) NOT NULL, + "МКОЗ" NUMERIC NOT NULL, + choroidal_thickness_center NUMERIC NOT NULL, + cts_foveola NUMERIC NOT NULL, + cts_sup_inner_fovea NUMERIC NOT NULL, + cts_sup_out_fovea NUMERIC NOT NULL, + total_volume NUMERIC NOT NULL, + average_volume NUMERIC NOT NULL, + rpe_status NUMERIC NOT NULL, + rpe_localisation NUMERIC NOT NULL, + cme_localisation NUMERIC NOT NULL, + serouz_rpe_detachment_localisation NUMERIC NOT NULL, + serouz_rpe_detachment_width NUMERIC NOT NULL, + serouz_rpe_detachment_height NUMERIC NOT NULL, + serouz_rpe_detachment_area NUMERIC NOT NULL, + hemorrhagic_rpe_detachment_localisation NUMERIC NOT NULL, + hemorrhagic_rpe_detachment_width NUMERIC NOT NULL, + hemorrhagic_rpe_detachment_heidgt NUMERIC NOT NULL, + hemorrhagic_rpe_detachment_area NUMERIC NOT NULL, + fibrovascular_rpe_detachment_localisation NUMERIC NOT NULL, + fibrovascular_rpe_detachment_width NUMERIC NOT NULL, + fibrovascular_rpe_detachment_heidgt NUMERIC NOT NULL, + fibrovascular_rpe_detachment_area NUMERIC NOT NULL, + drusenoid_detachment_rpe_localisation NUMERIC NOT NULL, + drusenoid_detachment_rpe_width NUMERIC NOT NULL, + drusenoid_detachment_rpe_height NUMERIC NOT NULL, + drusenoid_detachment_rpe_area NUMERIC NOT NULL, + druses_localisation NUMERIC NOT NULL, + druses_weigt NUMERIC NOT NULL, + druses_heigt NUMERIC NOT NULL, + dzuses_area NUMERIC NOT NULL, + fluid_under_rpe_area NUMERIC NOT NULL, + fluid_under_rpe_localisation NUMERIC NOT NULL, + ez_status NUMERIC NOT NULL, + ez_localisation NUMERIC NOT NULL, + myoidnz_status NUMERIC NOT NULL, + myoidnz_localisation NUMERIC NOT NULL, + rne_detachment_localisation NUMERIC NOT NULL, + rne_detachment_width NUMERIC NOT NULL, + rne_detachment_heigt NUMERIC NOT NULL, + rne_detachment_area NUMERIC NOT NULL, + hyperreflective_material_localisation NUMERIC NOT NULL, + hyperreflective_material_area NUMERIC NOT NULL, + FOREIGN KEY (appointment_id) REFERENCES appointments (id) +)''' + + CREATE_MEDICINES = '''CREATE TABLE IF NOT EXISTS medicines ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type VARCHAR(5) NOT NULL, + amount INTEGER NOT NULL, + appointment_id INTEGER NOT NULL, + FOREIGN KEY (appointment_id) REFERENCES appointments (id) +)''' + + # Pacients = "INSERT INTO pacients (name, sex, year_of_birthday) VALUES (?, ?, ?)" + Pacients = """INSERT INTO pacients (name, sex, year_of_birthday) VALUES ('Иван Иванов', 'М', 1990) +,('Мария Смирнова', 'Ж', 1995) +,('Алексей Петров', 'М', 1988) +,('Екатерина Фролова', 'Ж', 1992) +,('Дмитрий Сидоров', 'М', 1985) +,('Анна Кузнецова', 'Ж', 1997) +,('Сергей Васильев', 'М', 1982) +,('Ольга Попова', 'Ж', 1990) +,('Николай Орлов', 'М', 1989) +,('Елена Михайлова', 'Ж', 1994);""" + # Appointments = "INSERT INTO appointments (date, duration_of_the_disease, pacient_id) VALUES (?, ?, ?)" + Appointments = """INSERT INTO appointments (date, duration_of_the_disease, pacient_id) VALUES ('2025-04-01', 10, 1) +,('2025-04-15', 5, 1) + +,('2025-03-20', 14, 2) + +,('2025-02-28', 7, 3) +,('2025-04-10', 3, 3) + +,('2025-01-15', 20, 4) + +,('2025-03-05', 11, 5) +,('2025-04-22', 9, 5) + +,('2025-02-18', 15, 6) + +,('2025-03-01', 8, 7) +,('2025-03-25', 6, 7) + +,('2025-04-03', 10, 8) + +,('2025-02-14', 18, 9) +,('2025-03-30', 12, 9) + +,('2025-04-12', 7, 10); + """ + Appointmen_1_1 = """INSERT INTO eyes ( + eye, appointment_id, date, duration_of_the_disease, topkon, optopol, areds, + refraction, type_of_neovascularization, "МКОЗ", choroidal_thickness_center, + cts_foveola, cts_sup_inner_fovea, cts_sup_out_fovea, total_volume, average_volume, + rpe_status, rpe_localisation, cme_localisation, serouz_rpe_detachment_localisation, + serouz_rpe_detachment_width, serouz_rpe_detachment_height, serouz_rpe_detachment_area, + hemorrhagic_rpe_detachment_localisation, hemorrhagic_rpe_detachment_width, + hemorrhagic_rpe_detachment_heidgt, hemorrhagic_rpe_detachment_area, + fibrovascular_rpe_detachment_localisation, fibrovascular_rpe_detachment_width, + fibrovascular_rpe_detachment_heidgt, fibrovascular_rpe_detachment_area, + drusenoid_detachment_rpe_localisation, drusenoid_detachment_rpe_width, + drusenoid_detachment_rpe_height, drusenoid_detachment_rpe_area, + druses_localisation, druses_weigt, druses_heigt, dzuses_area, + fluid_under_rpe_area, fluid_under_rpe_localisation, ez_status, ez_localisation, + myoidnz_status, myoidnz_localisation, rne_detachment_localisation, + rne_detachment_width, rne_detachment_heigt, rne_detachment_area, + hyperreflective_material_localisation, hyperreflective_material_area +) VALUES ( + 'L', 1, '2025-04-15', 10, FALSE, TRUE, 'AREDS1', + '-2.0', 'Occult', 300, 160, 210, 190, 200, 9.0, 3.5, + 0, 2, 1, 1, 3.0, 2.0, 1.0, + 1, 1.5, 1.0, 0.5, 2, 3.5, 3.0, 1.5, + 1, 0.7, 0.5, 0.3, 3, 4.0, 2.5, 1.0, + 0.8, 2.0, 1, 2, 1, 1, 0, + 3.8, 2.0, 0, 0, 0 +),( + 'R', 1, '2025-04-16', 5, TRUE, FALSE, 'AREDS2', + '-1.5', 'Classic', 250, 150, 200, 180, 190, 8.5, 3.2, + 1, 1, 2, 0, 2.5, 1.5, 0.5, + 0, 1.2, 0.8, 0.3, 1, 2.8, 2.4, 1.0, + 0, 0.5, 0.3, 0.2, 2, 3.1, 1.8, 0.7, + 0.6, 1.5, 1, 2, 1, 1, 0, + 3.2, 1.5, 0, 0, 0 +),( + 'L', 2, '2025-04-15', 10, FALSE, TRUE, 'AREDS1', + '-2.0', 'Occult', 300, 160, 210, 190, 200, 9.0, 3.5, + 0, 2, 1, 1, 3.0, 2.0, 1.0, + 1, 1.5, 1.0, 0.5, 2, 3.5, 3.0, 1.5, + 1, 0.7, 0.5, 0.3, 3, 4.0, 2.5, 1.0, + 0.8, 2.0, 1, 2, 1, 1, 0, + 3.8, 2.0, 0, 0, 0 +),( + 'R', 2, '2025-04-16', 5, TRUE, FALSE, 'AREDS2', + '-1.5', 'Classic', 250, 150, 200, 180, 190, 8.5, 3.2, + 1, 1, 2, 0, 2.5, 1.5, 0.5, + 0, 1.2, 0.8, 0.3, 1, 2.8, 2.4, 1.0, + 0, 0.5, 0.3, 0.2, 2, 3.1, 1.8, 0.7, + 0.6, 1.5, 1, 2, 1, 1, 0, + 3.2, 1.5, 0, 0, 0 +),( + 'L', 3, '2025-04-15', 10, FALSE, TRUE, 'AREDS1', + '-2.0', 'Occult', 300, 160, 210, 190, 200, 9.0, 3.5, + 0, 2, 1, 1, 3.0, 2.0, 1.0, + 1, 1.5, 1.0, 0.5, 2, 3.5, 3.0, 1.5, + 1, 0.7, 0.5, 0.3, 3, 4.0, 2.5, 1.0, + 0.8, 2.0, 1, 2, 1, 1, 0, + 3.8, 2.0, 0, 0, 0 +),( + 'R', 3, '2025-04-16', 5, TRUE, FALSE, 'AREDS2', + '-1.5', 'Classic', 250, 150, 200, 180, 190, 8.5, 3.2, + 1, 1, 2, 0, 2.5, 1.5, 0.5, + 0, 1.2, 0.8, 0.3, 1, 2.8, 2.4, 1.0, + 0, 0.5, 0.3, 0.2, 2, 3.1, 1.8, 0.7, + 0.6, 1.5, 1, 2, 1, 1, 0, + 3.2, 1.5, 0, 0, 0 +),( + 'L', 4, '2025-04-15', 10, FALSE, TRUE, 'AREDS1', + '-2.0', 'Occult', 300, 160, 210, 190, 200, 9.0, 3.5, + 0, 2, 1, 1, 3.0, 2.0, 1.0, + 1, 1.5, 1.0, 0.5, 2, 3.5, 3.0, 1.5, + 1, 0.7, 0.5, 0.3, 3, 4.0, 2.5, 1.0, + 0.8, 2.0, 1, 2, 1, 1, 0, + 3.8, 2.0, 0, 0, 0 +),( + 'R', 4, '2025-04-16', 5, TRUE, FALSE, 'AREDS2', + '-1.5', 'Classic', 250, 150, 200, 180, 190, 8.5, 3.2, + 1, 1, 2, 0, 2.5, 1.5, 0.5, + 0, 1.2, 0.8, 0.3, 1, 2.8, 2.4, 1.0, + 0, 0.5, 0.3, 0.2, 2, 3.1, 1.8, 0.7, + 0.6, 1.5, 1, 2, 1, 1, 0, + 3.2, 1.5, 0, 0, 0 +),( + 'L', 5, '2025-04-15', 10, FALSE, TRUE, 'AREDS1', + '-2.0', 'Occult', 300, 160, 210, 190, 200, 9.0, 3.5, + 0, 2, 1, 1, 3.0, 2.0, 1.0, + 1, 1.5, 1.0, 0.5, 2, 3.5, 3.0, 1.5, + 1, 0.7, 0.5, 0.3, 3, 4.0, 2.5, 1.0, + 0.8, 2.0, 1, 2, 1, 1, 0, + 3.8, 2.0, 0, 0, 0 +),( + 'R', 5, '2025-04-16', 5, TRUE, FALSE, 'AREDS2', + '-1.5', 'Classic', 250, 150, 200, 180, 190, 8.5, 3.2, + 1, 1, 2, 0, 2.5, 1.5, 0.5, + 0, 1.2, 0.8, 0.3, 1, 2.8, 2.4, 1.0, + 0, 0.5, 0.3, 0.2, 2, 3.1, 1.8, 0.7, + 0.6, 1.5, 1, 2, 1, 1, 0, + 3.2, 1.5, 0, 0, 0 +),( + 'L', 6, '2025-04-15', 10, FALSE, TRUE, 'AREDS1', + '-2.0', 'Occult', 300, 160, 210, 190, 200, 9.0, 3.5, + 0, 2, 1, 1, 3.0, 2.0, 1.0, + 1, 1.5, 1.0, 0.5, 2, 3.5, 3.0, 1.5, + 1, 0.7, 0.5, 0.3, 3, 4.0, 2.5, 1.0, + 0.8, 2.0, 1, 2, 1, 1, 0, + 3.8, 2.0, 0, 0, 0 +),( + 'R', 6, '2025-04-16', 5, TRUE, FALSE, 'AREDS2', + '-1.5', 'Classic', 250, 150, 200, 180, 190, 8.5, 3.2, + 1, 1, 2, 0, 2.5, 1.5, 0.5, + 0, 1.2, 0.8, 0.3, 1, 2.8, 2.4, 1.0, + 0, 0.5, 0.3, 0.2, 2, 3.1, 1.8, 0.7, + 0.6, 1.5, 1, 2, 1, 1, 0, + 3.2, 1.5, 0, 0, 0 +),( + 'L', 7, '2025-04-15', 10, FALSE, TRUE, 'AREDS1', + '-2.0', 'Occult', 300, 160, 210, 190, 200, 9.0, 3.5, + 0, 2, 1, 1, 3.0, 2.0, 1.0, + 1, 1.5, 1.0, 0.5, 2, 3.5, 3.0, 1.5, + 1, 0.7, 0.5, 0.3, 3, 4.0, 2.5, 1.0, + 0.8, 2.0, 1, 2, 1, 1, 0, + 3.8, 2.0, 0, 0, 0 +),( + 'R', 7, '2025-04-16', 5, TRUE, FALSE, 'AREDS2', + '-1.5', 'Classic', 250, 150, 200, 180, 190, 8.5, 3.2, + 1, 1, 2, 0, 2.5, 1.5, 0.5, + 0, 1.2, 0.8, 0.3, 1, 2.8, 2.4, 1.0, + 0, 0.5, 0.3, 0.2, 2, 3.1, 1.8, 0.7, + 0.6, 1.5, 1, 2, 1, 1, 0, + 3.2, 1.5, 0, 0, 0 +),( + 'L', 8, '2025-04-15', 10, FALSE, TRUE, 'AREDS1', + '-2.0', 'Occult', 300, 160, 210, 190, 200, 9.0, 3.5, + 0, 2, 1, 1, 3.0, 2.0, 1.0, + 1, 1.5, 1.0, 0.5, 2, 3.5, 3.0, 1.5, + 1, 0.7, 0.5, 0.3, 3, 4.0, 2.5, 1.0, + 0.8, 2.0, 1, 2, 1, 1, 0, + 3.8, 2.0, 0, 0, 0 +),( + 'R', 1, '2025-04-16', 5, TRUE, FALSE, 'AREDS2', + '-1.5', 'Classic', 250, 150, 200, 180, 190, 8.5, 3.2, + 1, 1, 2, 0, 2.5, 1.5, 0.5, + 0, 1.2, 0.8, 0.3, 1, 2.8, 2.4, 1.0, + 0, 0.5, 0.3, 0.2, 2, 3.1, 1.8, 0.7, + 0.6, 1.5, 1, 2, 1, 1, 0, + 3.2, 1.5, 0, 0, 0 +),( + 'L', 9, '2025-04-15', 10, FALSE, TRUE, 'AREDS1', + '-2.0', 'Occult', 300, 160, 210, 190, 200, 9.0, 3.5, + 0, 2, 1, 1, 3.0, 2.0, 1.0, + 1, 1.5, 1.0, 0.5, 2, 3.5, 3.0, 1.5, + 1, 0.7, 0.5, 0.3, 3, 4.0, 2.5, 1.0, + 0.8, 2.0, 1, 2, 1, 1, 0, + 3.8, 2.0, 0, 0, 0 +),( + 'R', 9, '2025-04-16', 5, TRUE, FALSE, 'AREDS2', + '-1.5', 'Classic', 250, 150, 200, 180, 190, 8.5, 3.2, + 1, 1, 2, 0, 2.5, 1.5, 0.5, + 0, 1.2, 0.8, 0.3, 1, 2.8, 2.4, 1.0, + 0, 0.5, 0.3, 0.2, 2, 3.1, 1.8, 0.7, + 0.6, 1.5, 1, 2, 1, 1, 0, + 3.2, 1.5, 0, 0, 0 +),( + 'L', 10, '2025-04-15', 10, FALSE, TRUE, 'AREDS1', + '-2.0', 'Occult', 300, 160, 210, 190, 200, 9.0, 3.5, + 0, 2, 1, 1, 3.0, 2.0, 1.0, + 1, 1.5, 1.0, 0.5, 2, 3.5, 3.0, 1.5, + 1, 0.7, 0.5, 0.3, 3, 4.0, 2.5, 1.0, + 0.8, 2.0, 1, 2, 1, 1, 0, + 3.8, 2.0, 0, 0, 0 +),( + 'R', 10, '2025-04-16', 5, TRUE, FALSE, 'AREDS2', + '-1.5', 'Classic', 250, 150, 200, 180, 190, 8.5, 3.2, + 1, 1, 2, 0, 2.5, 1.5, 0.5, + 0, 1.2, 0.8, 0.3, 1, 2.8, 2.4, 1.0, + 0, 0.5, 0.3, 0.2, 2, 3.1, 1.8, 0.7, + 0.6, 1.5, 1, 2, 1, 1, 0, + 3.2, 1.5, 0, 0, 0 +),( + 'L', 11, '2025-04-15', 10, FALSE, TRUE, 'AREDS1', + '-2.0', 'Occult', 300, 160, 210, 190, 200, 9.0, 3.5, + 0, 2, 1, 1, 3.0, 2.0, 1.0, + 1, 1.5, 1.0, 0.5, 2, 3.5, 3.0, 1.5, + 1, 0.7, 0.5, 0.3, 3, 4.0, 2.5, 1.0, + 0.8, 2.0, 1, 2, 1, 1, 0, + 3.8, 2.0, 0, 0, 0 +),( + 'R', 11, '2025-04-16', 5, TRUE, FALSE, 'AREDS2', + '-1.5', 'Classic', 250, 150, 200, 180, 190, 8.5, 3.2, + 1, 1, 2, 0, 2.5, 1.5, 0.5, + 0, 1.2, 0.8, 0.3, 1, 2.8, 2.4, 1.0, + 0, 0.5, 0.3, 0.2, 2, 3.1, 1.8, 0.7, + 0.6, 1.5, 1, 2, 1, 1, 0, + 3.2, 1.5, 0, 0, 0 +),( + 'L', 12, '2025-04-15', 10, FALSE, TRUE, 'AREDS1', + '-2.0', 'Occult', 300, 160, 210, 190, 200, 9.0, 3.5, + 0, 2, 1, 1, 3.0, 2.0, 1.0, + 1, 1.5, 1.0, 0.5, 2, 3.5, 3.0, 1.5, + 1, 0.7, 0.5, 0.3, 3, 4.0, 2.5, 1.0, + 0.8, 2.0, 1, 2, 1, 1, 0, + 3.8, 2.0, 0, 0, 0 +),( + 'R', 12, '2025-04-16', 5, TRUE, FALSE, 'AREDS2', + '-1.5', 'Classic', 250, 150, 200, 180, 190, 8.5, 3.2, + 1, 1, 2, 0, 2.5, 1.5, 0.5, + 0, 1.2, 0.8, 0.3, 1, 2.8, 2.4, 1.0, + 0, 0.5, 0.3, 0.2, 2, 3.1, 1.8, 0.7, + 0.6, 1.5, 1, 2, 1, 1, 0, + 3.2, 1.5, 0, 0, 0 +),( + 'L', 13, '2025-04-15', 10, FALSE, TRUE, 'AREDS1', + '-2.0', 'Occult', 300, 160, 210, 190, 200, 9.0, 3.5, + 0, 2, 1, 1, 3.0, 2.0, 1.0, + 1, 1.5, 1.0, 0.5, 2, 3.5, 3.0, 1.5, + 1, 0.7, 0.5, 0.3, 3, 4.0, 2.5, 1.0, + 0.8, 2.0, 1, 2, 1, 1, 0, + 3.8, 2.0, 0, 0, 0 +),( + 'R', 13, '2025-04-16', 5, TRUE, FALSE, 'AREDS2', + '-1.5', 'Classic', 250, 150, 200, 180, 190, 8.5, 3.2, + 1, 1, 2, 0, 2.5, 1.5, 0.5, + 0, 1.2, 0.8, 0.3, 1, 2.8, 2.4, 1.0, + 0, 0.5, 0.3, 0.2, 2, 3.1, 1.8, 0.7, + 0.6, 1.5, 1, 2, 1, 1, 0, + 3.2, 1.5, 0, 0, 0 +),( + 'L', 14, '2025-04-15', 10, FALSE, TRUE, 'AREDS1', + '-2.0', 'Occult', 300, 160, 210, 190, 200, 9.0, 3.5, + 0, 2, 1, 1, 3.0, 2.0, 1.0, + 1, 1.5, 1.0, 0.5, 2, 3.5, 3.0, 1.5, + 1, 0.7, 0.5, 0.3, 3, 4.0, 2.5, 1.0, + 0.8, 2.0, 1, 2, 1, 1, 0, + 3.8, 2.0, 0, 0, 0 +),( + 'R', 14, '2025-04-16', 5, TRUE, FALSE, 'AREDS2', + '-1.5', 'Classic', 250, 150, 200, 180, 190, 8.5, 3.2, + 1, 1, 2, 0, 2.5, 1.5, 0.5, + 0, 1.2, 0.8, 0.3, 1, 2.8, 2.4, 1.0, + 0, 0.5, 0.3, 0.2, 2, 3.1, 1.8, 0.7, + 0.6, 1.5, 1, 2, 1, 1, 0, + 3.2, 1.5, 0, 0, 0 +);""" + + def __init__(self, db_name="database.db"): + super().__init__() + self.db = QSqlDatabase.addDatabase("QSQLITE") + self.db.setDatabaseName(db_name) + self.connection = sqlite3.connect(db_name) + self.cursor = self.connection.cursor() + self.execute_query(self.CREATE_PATIENTS) + self.execute_query(self.CREATE_APPOINTMENTS) + self.execute_query(self.CREATE_EYES) + # self.execute_query(self.Pacients, ) + # self.execute_query(self.Appointments, ) + # self.execute_query(self.Appointmen_1_1) + self.close_connection() + + def start_connection(self, db_name="database.db"): + self.connection = sqlite3.connect(db_name) + self.cursor = self.connection.cursor() + + def execute_query(self, query, params=()): + try: + self.cursor.execute(query, params) + self.connection.commit() + except sqlite3.Error as e: + print(f"Ошибка выполнения запроса: {e}") + + def fetch_results(self, query, params=()): + try: + self.cursor.execute(query, params) + return self.cursor.fetchall() + except sqlite3.Error as e: + print(f"Ошибка получения данных: {e}") + return [] + + def fetch_record_by_id(self, table, record_id): + self.start_connection() + query = "SELECT * FROM pacients WHERE id = :record_id" + params = (record_id) + self.cursor.execute(query, {"record_id": record_id}) + return self.cursor.fetchall() + + def update_pacient(self, name, sex, year_of_birthday, id): + self.start_connection() + query = "UPDATE pacients SET name = :name, sex = :sex, year_of_birthday = :year_of_birthday WHERE id = :id" + self.cursor.execute(query, {"name": name, "sex": sex, "year_of_birthday": year_of_birthday, "id": id,}) + self.connection.commit() + self.close_connection() + + def close_connection(self): + self.connection.close() diff --git a/src/bsmu/macula/plugins/db/edit_pacirnt_delegate.py b/src/bsmu/macula/plugins/db/edit_pacirnt_delegate.py new file mode 100644 index 0000000..5309cee --- /dev/null +++ b/src/bsmu/macula/plugins/db/edit_pacirnt_delegate.py @@ -0,0 +1,20 @@ +from PySide6.QtGui import QIcon +from PySide6.QtWidgets import QStyledItemDelegate, QWidget, QHBoxLayout, QPushButton, QMessageBox + + +class EditPacientDelegate(QStyledItemDelegate): + def __init__(self, icon_path, parent=None, callback=None): + super().__init__(parent) + self.icon_path = icon_path + self.callback = callback # Функция, которая вызывается при нажатии кнопки + + def paint(self, painter, option, index): + """Рисуем кнопку в ячейке""" + icon = QIcon(self.icon_path) # 🔹 Здесь нужна иконка (например, карандаш) + icon.paint(painter, option.rect) + + def editorEvent(self, event, model, option, index): + """Обрабатываем нажатие на кнопку""" + if event.type() == event.Type.MouseButtonPress: + self.callback(model.data(model.index(index.row(), 0))) + return True \ No newline at end of file diff --git a/src/bsmu/macula/plugins/images/edit.png b/src/bsmu/macula/plugins/images/edit.png new file mode 100644 index 0000000..6b44614 Binary files /dev/null and b/src/bsmu/macula/plugins/images/edit.png differ diff --git a/src/bsmu/macula/records/eye_info_data.py b/src/bsmu/macula/records/eye_info_data.py new file mode 100644 index 0000000..c0efc41 --- /dev/null +++ b/src/bsmu/macula/records/eye_info_data.py @@ -0,0 +1,126 @@ +from dataclasses import dataclass, field +from datetime import date +from typing import Optional + +from PySide6.QtSql import QSqlQueryModel + + +@dataclass +class Measurement: + """Базовый класс для измерений с локализацией и размерами""" + location: Optional[str] = None + width: Optional[float] = None + height: Optional[float] = None + area: Optional[float] = None + + @classmethod + def from_model(cls, model: QSqlQueryModel, row: int, + loc_idx: int, w_idx: int, h_idx: int, a_idx: int): + return cls( + location=model.data(model.index(row, loc_idx)), + width=float(model.data(model.index(row, w_idx))) if model.data(model.index(row, w_idx)) else None, + height=float(model.data(model.index(row, h_idx))) if model.data(model.index(row, h_idx)) else None, + area=float(model.data(model.index(row, a_idx))) if model.data(model.index(row, a_idx)) else None + ) + +@dataclass +class ZoneStatus: + """Состояние анатомических зон""" + condition: Optional[str] = None + defect_location: Optional[str] = None + + @classmethod + def from_model(cls, model: QSqlQueryModel, row: int, + cond_idx: int, loc_idx: int): + return cls( + condition=model.data(model.index(row, cond_idx)), + defect_location=model.data(model.index(row, loc_idx)) + ) + +@dataclass +class PatientExamData: + # Основные метаданные + visit_date: Optional[date] = None + disease_duration: Optional[str] = None + tomograph_type: Optional[str] = "Топкон" # Добавлено поле типа томографа + areds_criteria: Optional[str] = None + refraction: Optional[str] = None + neovascularization_type: Optional[str] = None + + # Общие измерения сетчатки + choroidal_center_thickness: Optional[float] = None + foveal_retinal_thickness: Optional[float] = None + total_retinal_volume: Optional[float] = None + average_retinal_volume: Optional[float] = None + + # Состояние РПЭ + rpe_status: ZoneStatus = field(default_factory=ZoneStatus) + cmo_location: Optional[str] = None + + # Типы ОПЭ + serous_ped: Measurement = field(default_factory=Measurement) + hemorrhagic_ped: Measurement = field(default_factory=Measurement) + fibrovascular_ped: Measurement = field(default_factory=Measurement) + drusenoid_ped: Measurement = field(default_factory=Measurement) + + # Друзы + drusen: Measurement = field(default_factory=Measurement) + + # Жидкость под РПЭ + sub_rpe_fluid: Measurement = field(default_factory=Measurement) + + # Состояние зон + ellipsoid_zone: ZoneStatus = field(default_factory=ZoneStatus) + myoid_zone: ZoneStatus = field(default_factory=ZoneStatus) + + # Патологии + nsr_detachment: Measurement = field(default_factory=Measurement) + hyperreflective_material: Measurement = field(default_factory=Measurement) + + @classmethod + def from_query_model(cls, model: QSqlQueryModel, row: int): + def get_date(idx): + val = model.data(model.index(row, idx)) + return val.toPyDate() if hasattr(val, 'toPyDate') else val + + def get_str(idx): + val = model.data(model.index(row, idx)) + return str(val) if val else None + + def get_float(idx): + val = model.data(model.index(row, idx)) + try: + return float(val) if val else None + except (ValueError, TypeError): + return None + + return cls( + visit_date=get_date(3), + disease_duration=get_str(4), + tomograph_type="Топкон", # Установлено значение по умолчанию + areds_criteria=get_str(7), + refraction=get_str(8), + neovascularization_type=get_str(9), + choroidal_center_thickness=get_float(10), + foveal_retinal_thickness=get_float(11), + total_retinal_volume=get_float(14), + average_retinal_volume=get_float(15), + rpe_status=ZoneStatus.from_model(model, row, 16, 17), + cmo_location=get_str(18), + serous_ped=Measurement.from_model(model, row, 19, 20, 21, 22), + hemorrhagic_ped=Measurement.from_model(model, row, 23, 24, 25, 26), + fibrovascular_ped=Measurement.from_model(model, row, 27, 28, 29, 30), + drusenoid_ped=Measurement.from_model(model, row, 31, 32, 33, 34), + drusen=Measurement.from_model(model, row, 35, 36, 37, 38), + sub_rpe_fluid=Measurement( + area=get_float(39), + location=get_str(40) + ), + ellipsoid_zone=ZoneStatus.from_model(model, row, 41, 42), + myoid_zone=ZoneStatus.from_model(model, row, 43, 44), + nsr_detachment=Measurement.from_model(model, row, 45, 46, 47, 48), + hyperreflective_material=Measurement( + location=get_str(49), + area=get_float(50) + ) + ) \ No newline at end of file diff --git a/src/bsmu/macula/records/patient.py b/src/bsmu/macula/records/patient.py new file mode 100644 index 0000000..ce23e1d --- /dev/null +++ b/src/bsmu/macula/records/patient.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + + +@dataclass +class Patient: + id: int + name: str + sex: str + year_of_birthday: int diff --git a/src/bsmu/macula/version.py b/src/bsmu/macula/version.py index 93b60a1..8411e55 100644 --- a/src/bsmu/macula/version.py +++ b/src/bsmu/macula/version.py @@ -1 +1 @@ -__version__ = '0.5.1' +__version__ = '0.6.1' diff --git a/src/bsmu/macula/widgets/eye_data_widget.py b/src/bsmu/macula/widgets/eye_data_widget.py new file mode 100644 index 0000000..f77440b --- /dev/null +++ b/src/bsmu/macula/widgets/eye_data_widget.py @@ -0,0 +1,120 @@ +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QWidget, QVBoxLayout, QGroupBox, QFormLayout, QLabel, QScrollArea + +from bsmu.macula.records.eye_info_data import PatientExamData + + +class EyeDataWidget(QWidget): + def __init__(self, eye_data: PatientExamData, parent=None): + super().__init__(parent) + self.eye_data = eye_data + self.init_ui() + + def init_ui(self): + layout = QVBoxLayout(self) + scroll_area = self.create_scrollable_area() + layout.addWidget(scroll_area) + self.setLayout(layout) + + def create_block(self, title, fields): + """Создание блока с формой для группы полей""" + group_box = QGroupBox(title) + form_layout = QFormLayout() + + for label_text, field_value in fields: + label = QLabel(label_text) + value = str(field_value) if field_value is not None else "" + value_label = QLabel(value) + value_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + form_layout.addRow(label, value_label) + + group_box.setLayout(form_layout) + return group_box + + def create_scrollable_area(self): + """Создаёт область прокрутки с блоками данных""" + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + + scroll_content = QWidget() + scroll_layout = QVBoxLayout() + + scroll_layout.addWidget(self.create_block("Обследование", [ + ("Дата посещения", self.eye_data.visit_date), + ("Продолжительность заболевания", self.eye_data.disease_duration), + ("Тип томографа ", self.eye_data.tomograph_type), + ("Критерий AREDS", self.eye_data.areds_criteria), + ("Рефракция", self.eye_data.refraction), + ("Тип неоваскуляризации", self.eye_data.neovascularization_type) + ])) + scroll_layout.addWidget(self.create_block("Ретинальные показатели", [ + ("Толщина хориоидеи в центре", self.eye_data.choroidal_center_thickness), + ("Толщина сетчатки в фовеоле", self.eye_data.foveal_retinal_thickness), + ("Общий объем", self.eye_data.total_retinal_volume), + ("Средний объем", self.eye_data.average_retinal_volume) + ])) + scroll_layout.addWidget(self.create_block("", [ + ("Состояние РПЭ", self.eye_data.rpe_status.condition), + ("Локализация дефектов РПЭ", self.eye_data.rpe_status.defect_location), + ("Локализация кистозного макулярного отека", self.eye_data.cmo_location) + ])) + scroll_layout.addWidget(QLabel("Отслойки РПЭ")) + scroll_layout.addWidget(self.create_block("Серозная ОПЭ", [ + ("Локализация", self.eye_data.serous_ped.location), + ("Ширина", self.eye_data.serous_ped.width), + ("Высота", self.eye_data.serous_ped.height), + ("Площадь", self.eye_data.serous_ped.area) + ])) + scroll_layout.addWidget(self.create_block("Геморрагическая ОПЭ", [ + ("Локализация", self.eye_data.hemorrhagic_ped.location), + ("Ширина", self.eye_data.hemorrhagic_ped.width), + ("Высота", self.eye_data.hemorrhagic_ped.height), + ("Площадь", self.eye_data.hemorrhagic_ped.area) + ])) + scroll_layout.addWidget(self.create_block("Фиброваскулярная ОПЭ", [ + ("Локализация", self.eye_data.fibrovascular_ped.location), + ("Ширина", self.eye_data.fibrovascular_ped.width), + ("Высота", self.eye_data.fibrovascular_ped.height), + ("Площадь", self.eye_data.fibrovascular_ped.area) + ])) + scroll_layout.addWidget(self.create_block("Друзеноидная ОПЭ", [ + ("Локализация", self.eye_data.drusenoid_ped.location), + ("Ширина", self.eye_data.drusenoid_ped.width), + ("Высота", self.eye_data.drusenoid_ped.height), + ("Площадь", self.eye_data.drusenoid_ped.area) + ])) + scroll_layout.addWidget(self.create_block("Друзы", [ + ("Локализация", self.eye_data.drusen.location), + ("Ширина", self.eye_data.drusen.width), + ("Высота", self.eye_data.drusen.height), + ("Площадь", self.eye_data.drusen.area) + ])) + scroll_layout.addWidget(self.create_block("Жидкость под РПЭ", [ + ("Пощадь", self.eye_data.sub_rpe_fluid.area), + ("Локализация", self.eye_data.sub_rpe_fluid.location) + ])) + scroll_layout.addWidget(self.create_block("Эллипсоидная зона", [ + ("Состояние", self.eye_data.ellipsoid_zone.condition), + ("Локализация дефектов", self.eye_data.ellipsoid_zone.defect_location) + ])) + scroll_layout.addWidget(self.create_block("Миоидная зона", [ + ("Состояние", self.eye_data.myoid_zone.condition), + ("Локализация дефектов", self.eye_data.myoid_zone.defect_location) + ])) + scroll_layout.addWidget(self.create_block("Отслойка нейросенсорной сетчатки", [ + ("Локализация", self.eye_data.nsr_detachment.location), + ("Ширина", self.eye_data.nsr_detachment.width), + ("Высота", self.eye_data.nsr_detachment.height), + ("Площадь", self.eye_data.nsr_detachment.area) + ])) + scroll_layout.addWidget(self.create_block("Гиперрефлективный материал", [ + ("Локализация", self.eye_data.hyperreflective_material.location), + ("Площадь", self.eye_data.hyperreflective_material.area) + ])) + + # Добавьте остальные блоки по аналогии... + + scroll_content.setLayout(scroll_layout) + scroll_area.setWidget(scroll_content) + + return scroll_area \ No newline at end of file