/
O.S.Prog
/
InstrumentProtocol
Обзор
Документация
Войти
/
O.S.Prog
/
InstrumentProtocol
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
ui/editor_window.py
833 строки
39 KB
O.S.Prog
Initial commit: InstrumentProtocol v2.0
17 июл 2026, 15:11
17 июл 2026, 15:11
1e845bd
Код
Авторство
О чём код?
import os from datetime import datetime from PyQt6.QtWidgets import * from PyQt6.QtCore import * from PyQt6.QtGui import * from widgets.formatting_toolbar import FormattingToolbar from widgets.patient_info_widget import PatientInfoWidget from core.protocol_manager import ProtocolManager from core.settings_manager import SettingsManager from utils.file_utils import get_protocols_folder class EditorWindow(QMainWindow): def __init__(self, research_type, login="", full_name="", readonly=False, encryption_key=None): super().__init__() self.research_type = research_type self.login = login self.full_name = full_name self.readonly = readonly self.protocol_manager = ProtocolManager(encryption_key) self.settings = SettingsManager() self.current_filepath = None title_text = f"Протокол {self._get_protocol_full_name(research_type)} исследования - Редактор" if readonly: title_text = "[ТОЛЬКО ПРОСМОТР] " + title_text self.setWindowTitle(title_text) self.setGeometry(150, 150, 900, 750) self.create_menu() central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout() # Название учреждения self.hospital_name = QLineEdit() self.hospital_name.setPlaceholderText("Название лечебного учреждения") self.hospital_name.setStyleSheet("font-size: 14px; font-weight: bold; padding: 5px;") last_hospital = self.settings.get_last_hospital() self.hospital_name.setText(last_hospital or self.settings.get("hospital_name", "")) main_layout.addWidget(self.hospital_name) # Заголовок протокола protocol_full = self._get_protocol_full_name(research_type) self.protocol_title = QLabel(f"ПРОТОКОЛ {protocol_full.upper()} ИССЛЕДОВАНИЯ") self.protocol_title.setStyleSheet("font-size: 14px; font-weight: bold; color: #2c3e50;") self.protocol_title.setAlignment(Qt.AlignmentFlag.AlignCenter) main_layout.addWidget(self.protocol_title) # Дата и время dt_layout = QHBoxLayout() dt_layout.addWidget(QLabel("Дата исследования:")) self.exam_date = QLineEdit() self.exam_date.setPlaceholderText("дд.мм.гггг") self.exam_date.setInputMask("99.99.9999") self.exam_date.setText(datetime.now().strftime("%d.%m.%Y")) self.exam_date.setStyleSheet("font-size: 14px; padding: 5px; width: 120px;") self.exam_date.editingFinished.connect(self._validate_exam_date) dt_layout.addWidget(self.exam_date) dt_layout.addWidget(QLabel(" Время исследования:")) self.exam_time = QLineEdit() self.exam_time.setPlaceholderText("чч:мм") self.exam_time.setInputMask("99:99") self.exam_time.setText(datetime.now().strftime("%H:%M")) self.exam_time.setStyleSheet("font-size: 14px; padding: 5px; width: 80px;") self.exam_time.editingFinished.connect(self._validate_exam_time) dt_layout.addWidget(self.exam_time) dt_layout.addStretch() main_layout.addLayout(dt_layout) # Пациент self.patient_widget = PatientInfoWidget() main_layout.addWidget(self.patient_widget) # Заготовка для тулбара self.toolbar_widget = QWidget() self.toolbar_widget_layout = QHBoxLayout() self.toolbar_widget.setLayout(self.toolbar_widget_layout) main_layout.addWidget(self.toolbar_widget) # ОПИСАНИЕ desc_label = QLabel("ОПИСАНИЕ:") desc_label.setStyleSheet("font-size: 14px; font-weight: bold; margin-top: 10px;") main_layout.addWidget(desc_label) self.description_editor = QTextEdit() self.description_editor.setStyleSheet("font-size: 14px; font-family: 'Times New Roman';") self.description_editor.setAcceptRichText(False) self.description_editor.setTabStopDistance(40) self.description_editor.textChanged.connect(self.on_text_changed) main_layout.addWidget(self.description_editor) # ЗАКЛЮЧЕНИЕ concl_label = QLabel("ЗАКЛЮЧЕНИЕ:") concl_label.setStyleSheet("font-size: 14px; font-weight: bold; margin-top: 10px;") main_layout.addWidget(concl_label) self.conclusion_editor = QTextEdit() self.conclusion_editor.setStyleSheet("font-size: 14px; font-family: 'Times New Roman';") self.conclusion_editor.setAcceptRichText(False) self.conclusion_editor.setTabStopDistance(40) self.conclusion_editor.textChanged.connect(self.on_text_changed) main_layout.addWidget(self.conclusion_editor) # Тулбар self.toolbar = FormattingToolbar(self.description_editor, self.conclusion_editor, self.settings, self.research_type, self.login) self.toolbar_widget_layout.addWidget(self.toolbar) # Кнопки btn_layout = QHBoxLayout() self.new_btn = QPushButton("📄 НОВЫЙ ПРОТОКОЛ") self.new_btn.setStyleSheet(self._btn_style("#3498db", "#2980b9")) self.new_btn.clicked.connect(self.new_protocol) btn_layout.addWidget(self.new_btn) self.save_btn = QPushButton("💾 СОХРАНИТЬ") self.save_btn.setStyleSheet(self._btn_style("#27ae60", "#219a52")) self.save_btn.clicked.connect(self.save_protocol) btn_layout.addWidget(self.save_btn) print_btn = QPushButton("🖨️ ПЕЧАТЬ") print_btn.setStyleSheet(self._btn_style("#e67e22", "#d35400")) print_btn.clicked.connect(self.print_protocol) btn_layout.addWidget(print_btn) pdf_btn = QPushButton("📄 ЭКСПОРТ В PDF") pdf_btn.setStyleSheet(self._btn_style("#8e44ad", "#732d91")) pdf_btn.clicked.connect(self.export_pdf) btn_layout.addWidget(pdf_btn) back_btn = QPushButton("↩ Назад") back_btn.setStyleSheet(self._btn_style("#95a5a6", "#7f8c8d")) back_btn.clicked.connect(self.go_back) btn_layout.addWidget(back_btn) main_layout.addLayout(btn_layout) central_widget.setLayout(main_layout) self.setup_statusbar() if readonly: self.set_readonly_mode() def _btn_style(self, bg, hover): return f""" QPushButton {{ font-size: 14px; font-weight: bold; padding: 10px; background-color: {bg}; color: white; border: none; border-radius: 5px; }} QPushButton:hover {{ background-color: {hover}; }} """ # ========== МЕНЮ ========== def create_menu(self): menubar = self.menuBar() file_menu = menubar.addMenu("Файл") new_action = QAction("📄 Новый протокол", self) new_action.triggered.connect(self.new_protocol) file_menu.addAction(new_action) save_action = QAction("💾 Сохранить", self) save_action.setShortcut(QKeySequence.StandardKey.Save) save_action.triggered.connect(self.save_protocol) file_menu.addAction(save_action) file_menu.addSeparator() print_action = QAction("🖨️ Печать", self) print_action.setShortcut(QKeySequence.StandardKey.Print) print_action.triggered.connect(self.print_protocol) file_menu.addAction(print_action) pdf_action = QAction("📄 Экспорт в PDF", self) pdf_action.triggered.connect(self.export_pdf) file_menu.addAction(pdf_action) file_menu.addSeparator() exit_action = QAction("🚪 Выход", self) exit_action.setShortcut(QKeySequence.StandardKey.Quit) exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) archive_menu = menubar.addMenu("Архив") show_archive_action = QAction("📋 Показать архив", self) show_archive_action.triggered.connect(self.show_archive) archive_menu.addAction(show_archive_action) archive_menu.addSeparator() backup_action = QAction("💾 Создать резервную копию", self) backup_action.triggered.connect(self.create_backup) archive_menu.addAction(backup_action) restore_action = QAction("📂 Восстановить из резервной копии", self) restore_action.triggered.connect(self.restore_backup) archive_menu.addAction(restore_action) help_menu = menubar.addMenu("Справка") guide_action = QAction("📖 Руководство", self) guide_action.triggered.connect(self.show_guide) help_menu.addAction(guide_action) help_menu.addSeparator() about_action = QAction("ℹ️ О программе", self) about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) help_menu.addSeparator() feedback_action = QAction("✉️ Оставить отзыв", self) feedback_action.triggered.connect(self.show_feedback) help_menu.addAction(feedback_action) zoom_in_action = QAction("Увеличить", self) zoom_in_action.setShortcut(QKeySequence("Ctrl+=")) zoom_in_action.triggered.connect(self.zoom_in) self.addAction(zoom_in_action) zoom_out_action = QAction("Уменьшить", self) zoom_out_action.setShortcut(QKeySequence("Ctrl+-")) zoom_out_action.triggered.connect(self.zoom_out) self.addAction(zoom_out_action) zoom_reset_action = QAction("Сбросить масштаб", self) zoom_reset_action.setShortcut(QKeySequence("Ctrl+0")) zoom_reset_action.triggered.connect(self.reset_zoom) self.addAction(zoom_reset_action) # ========== СТАТУС-БАР ========== def setup_statusbar(self): self.statusbar = self.statusBar() self.zoom_label = QLabel("Масштаб: 100%") self.zoom_label.setStyleSheet("padding: 0 10px;") zoom_in_btn = QPushButton("🔍+") zoom_in_btn.setFixedSize(35, 22) zoom_in_btn.clicked.connect(self.zoom_in) zoom_out_btn = QPushButton("🔍-") zoom_out_btn.setFixedSize(35, 22) zoom_out_btn.clicked.connect(self.zoom_out) zoom_reset_btn = QPushButton("100%") zoom_reset_btn.setFixedSize(45, 22) zoom_reset_btn.clicked.connect(self.reset_zoom) self.statusbar.addPermanentWidget(zoom_out_btn) self.statusbar.addPermanentWidget(zoom_in_btn) self.statusbar.addPermanentWidget(zoom_reset_btn) self.statusbar.addPermanentWidget(self.zoom_label) # ========== READONLY ========== def set_readonly_mode(self): self.readonly = True self.hospital_name.setReadOnly(True) self.exam_date.setReadOnly(True) self.exam_time.setReadOnly(True) self.patient_widget.fio_input.setReadOnly(True) self.patient_widget.birth_input.setReadOnly(True) self.description_editor.setReadOnly(True) self.conclusion_editor.setReadOnly(True) self.toolbar.setEnabled(False) self.new_btn.setEnabled(False) self.new_btn.setVisible(False) self.save_btn.setVisible(False) self.save_btn.setEnabled(False) # ========== ЗУМ ========== def zoom_in(self): self._change_zoom(10) def zoom_out(self): self._change_zoom(-10) def reset_zoom(self): self._apply_zoom(100) def _change_zoom(self, delta): current = self.settings.get("ui_zoom", 100) new = max(50, min(200, current + delta)) self._apply_zoom(new) def _apply_zoom(self, percent): self.settings.set("ui_zoom", percent) self.zoom_label.setText(f"Масштаб: {percent}%") # Применяем к редакторам font_size = max(6, int(14 * percent / 100)) font = QFont("Times New Roman", font_size) self.description_editor.setFont(font) self.conclusion_editor.setFont(font) # Поля ввода input_size = max(6, int(14 * percent / 100)) self.hospital_name.setStyleSheet(f"font-size: {input_size}px; font-weight: bold; padding: 5px;") self.exam_date.setStyleSheet(f"font-size: {input_size}px; padding: 5px; width: 120px;") self.exam_time.setStyleSheet(f"font-size: {input_size}px; padding: 5px; width: 80px;") self.patient_widget.fio_input.setStyleSheet(f"font-size: {input_size}px; padding: 5px;") self.patient_widget.birth_input.setStyleSheet(f"font-size: {input_size}px; padding: 5px;") # ========== НОВЫЙ ПРОТОКОЛ ========== def new_protocol(self): if not self.readonly and not self._check_unsaved(): return self.current_filepath = None self.hospital_name.setText(self.settings.get_last_hospital() or self.settings.get("hospital_name", "")) self.exam_date.setText(datetime.now().strftime("%d.%m.%Y")) self.exam_time.setText(datetime.now().strftime("%H:%M")) self.patient_widget.fio_input.clear() self.patient_widget.birth_input.clear() self.description_editor.clear() self.conclusion_editor.clear() self.setWindowTitle(f"Протокол {self._get_protocol_full_name(self.research_type)} исследования - Редактор") def on_text_changed(self): title = f"Протокол {self._get_protocol_full_name(self.research_type)} исследования - Редактор" if self.description_editor.toPlainText().strip() or self.conclusion_editor.toPlainText().strip(): self.setWindowTitle(f"* {title}") else: self.setWindowTitle(title) # ========== СОХРАНЕНИЕ ========== def save_protocol(self): fio = self.patient_widget.get_fio() if not fio: QMessageBox.warning(self, "Ошибка", "Введите ФИО пациента") return valid, msg = self.patient_widget.validate_birth_date() if not valid: QMessageBox.warning(self, "Неверная дата", msg) self.patient_widget.birth_input.setFocus() self.patient_widget.birth_input.selectAll() return # Проверяем дату исследования try: from datetime import datetime exam_text = self.exam_date.text().strip() if exam_text and len(exam_text) == 10: day = int(exam_text[:2]) month = int(exam_text[3:5]) year = int(exam_text[6:10]) exam_date = datetime(year, month, day) if exam_date > datetime.now(): QMessageBox.warning(self, "Неверная дата", "Введите корректную дату исследования.") self.exam_date.setFocus() return except: QMessageBox.warning(self, "Неверная дата", "Введите корректную дату исследования.") self.exam_date.setFocus() return # Проверяем время исследования try: time_text = self.exam_time.text().strip() if time_text and len(time_text) == 5: hour = int(time_text[:2]) minute = int(time_text[3:5]) if hour > 23 or minute > 59: raise ValueError except: QMessageBox.warning(self, "Неверное время", "Введите корректное время (00:00–23:59).") self.exam_time.setFocus() return # Если файл уже существует — спрашиваем if self.current_filepath: msg = QMessageBox(self) msg.setWindowTitle("Сохранение протокола") msg.setText("Этот протокол уже существует.") msg.setInformativeText("Что вы хотите сделать?") btn_overwrite = msg.addButton("Перезаписать", QMessageBox.ButtonRole.AcceptRole) btn_new = msg.addButton("Создать новый", QMessageBox.ButtonRole.AcceptRole) btn_cancel = msg.addButton("Отмена", QMessageBox.ButtonRole.RejectRole) msg.exec() clicked = msg.clickedButton() if clicked == btn_cancel: return elif clicked == btn_new: self.current_filepath = None # создаст новый файл # иначе — перезаписываем существующий success, filepath = self.protocol_manager.save_protocol( fio=fio, birth_date=self.patient_widget.get_birth_date(), hospital=self.hospital_name.text(), research_type=self.research_type, description=self.description_editor.toPlainText(), conclusion=self.conclusion_editor.toPlainText(), exam_date=self.exam_date.text(), exam_time=self.exam_time.text(), existing_filepath=self.current_filepath, doctor_name=self.full_name ) if success: self.current_filepath = filepath self.setWindowTitle(f"Протокол {self._get_protocol_full_name(self.research_type)} исследования - Редактор") QMessageBox.information(self, "Успех", f"Протокол сохранён!\n{os.path.basename(filepath)}") else: QMessageBox.critical(self, "Ошибка", "Не удалось сохранить протокол") # ========== ПЕЧАТЬ ========== def print_protocol(self): from core.smart_print import SmartPrint smart_print = SmartPrint(self.settings) smart_print.print_protocol( self, self.hospital_name.text(), self.research_type, self.patient_widget.get_fio(), self.patient_widget.get_birth_date(), self.exam_date.text(), self.exam_time.text(), self.description_editor.toPlainText(), self.conclusion_editor.toPlainText(), self.full_name ) # ========== PDF ========== def export_pdf(self): fio = self.patient_widget.get_fio() if not fio: QMessageBox.warning(self, "Ошибка", "Введите ФИО пациента") return pdf_folder = self.settings.get("pdf_export_folder", "") if not pdf_folder: QMessageBox.warning(self, "Ошибка", "Папка для PDF не настроена.\nЗайдите в Настройки.") return from datetime import datetime as dt from PyQt6.QtGui import QTextDocument from PyQt6.QtPrintSupport import QPrinter from utils.text_utils import text_to_html fio_clean = fio.replace(" ", "_") date_str = dt.now().strftime("%Y-%m-%d_%H-%M") filename = f"{fio_clean}_{date_str}.pdf" filepath = os.path.join(pdf_folder, filename) hospital = self.hospital_name.text() or "ЛЕЧЕБНОЕ УЧРЕЖДЕНИЕ" exam_date = self.exam_date.text() or dt.now().strftime("%d.%m.%Y") exam_time = self.exam_time.text() or dt.now().strftime("%H:%M") birth_date = self.patient_widget.get_birth_date() or "" desc_html = text_to_html(self.description_editor.toPlainText()) concl_html = text_to_html(self.conclusion_editor.toPlainText()) font_size = 14 html = f""" <!DOCTYPE html><html><head><meta charset="UTF-8"><style> body {{ font-family: 'Times New Roman', serif; font-size: {font_size}pt; margin: 1.5cm; line-height: 1.3; }} .hospital {{ text-align: center; font-weight: bold; font-size: {font_size}pt; margin-bottom: 10px; }} .protocol_title {{ text-align: center; font-weight: bold; font-size: {font_size+2}pt; margin-bottom: 10px; }} .datetime {{ font-size: {font_size}pt; margin-bottom: 15px; }} .patient {{ margin-bottom: 12px; font-size: {font_size}pt; }} .section_label {{ font-weight: bold; font-size: {font_size}pt; margin-top: 15px; margin-bottom: 5px; }} .divider {{ border: none; border-top: 1px solid #2c3e50; margin: 5px 0 10px 0; }} .content {{ text-align: justify; white-space: pre-wrap; font-size: {font_size}pt; }} .signature {{ text-align: right; margin-top: 30px; font-size: {font_size}pt; }} p {{ margin: 0 0 4px 0; }} </style></head><body> <div class="hospital">{hospital}</div> <div class="protocol_title">ПРОТОКОЛ {self._get_protocol_full_name(self.research_type).upper()} ИССЛЕДОВАНИЯ</div> <div class="datetime">Дата исследования: {exam_date} Время исследования: {exam_time}</div> <div class="patient"><strong>ФИО пациента:</strong> {fio}<br><strong>Дата рождения пациента:</strong> {birth_date}</div> <div class="section_label">ДАННЫЕ ИССЛЕДОВАНИЯ:</div><hr class="divider"><div class="content">{desc_html}</div> <div class="section_label">ЗАКЛЮЧЕНИЕ:</div><hr class="divider"><div class="content">{concl_html}</div> <div class="signature">Врач: ________________________ ({self.full_name})</div> </body></html>""" try: printer = QPrinter(QPrinter.PrinterMode.HighResolution) printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat) printer.setOutputFileName(filepath) doc = QTextDocument() doc.setHtml(html) doc.print(printer) QMessageBox.information(self, "Экспорт выполнен", f"✅ Протокол экспортирован в PDF:\n{filepath}") except Exception as e: QMessageBox.critical(self, "Ошибка", f"Не удалось экспортировать PDF:\n{str(e)}") # ========== АРХИВ / БЕКАП ========== def show_archive(self): from ui.archive_window import ArchiveWindow self.archive_window = ArchiveWindow( parent=self, research_type=self.research_type, readonly=False, full_name=self.full_name, encryption_key=self.protocol_manager.encryption.get_key() if self.protocol_manager.encryption else None ) self.archive_window.protocol_selected.connect(self._load_from_archive) self.archive_window.show() def _load_from_archive(self, filepath, fio, birth_date, hospital, rtype, description, conclusion, exam_date, exam_time, doctor_name, readonly): if readonly: from ui.editor_window import EditorWindow self.viewer = EditorWindow(rtype, "", "", readonly=True, encryption_key=self.protocol_manager.encryption.get_key() if self.protocol_manager.encryption else None) self.viewer.current_filepath = filepath self.viewer.hospital_name.setText(hospital) self.viewer.patient_widget.set_fio(fio) self.viewer.patient_widget.set_birth_date(birth_date) if exam_date: self.viewer.exam_date.setText(exam_date) if exam_time: self.viewer.exam_time.setText(exam_time) self.viewer.description_editor.setPlainText(description) self.viewer.conclusion_editor.setPlainText(conclusion) self.viewer.setWindowTitle(f"[ТОЛЬКО ПРОСМОТР] Протокол {self._get_protocol_full_name(rtype)} исследования - Редактор") self.viewer.show() else: self.research_type = rtype self.setWindowTitle(f"Протокол {self._get_protocol_full_name(rtype)} исследования - Редактор") self.current_filepath = filepath self.hospital_name.setText(hospital) self.patient_widget.set_fio(fio) self.patient_widget.set_birth_date(birth_date) if exam_date: self.exam_date.setText(exam_date) if exam_time: self.exam_time.setText(exam_time) self.description_editor.setPlainText(description) self.conclusion_editor.setPlainText(conclusion) self.setWindowTitle(f"Протокол {rtype.lower()} исследования - Редактор") def create_backup(self): from PyQt6.QtWidgets import QFileDialog from core.backup import create_backup last_path = self.settings.get("last_backup_path", os.path.expanduser("~/Documents")) folder = QFileDialog.getExistingDirectory(self, "Выберите папку для резервной копии", last_path) if not folder: return if not os.path.exists(folder): os.makedirs(folder, exist_ok=True) self.settings.set("last_backup_path", folder) key = self.protocol_manager.encryption.get_key() if self.protocol_manager.encryption else None success, message, count, backup_path = create_backup(folder, key) if success: self.settings.set("last_backup_date", datetime.now().strftime("%d.%m.%Y")) QMessageBox.information(self, "Резервная копия создана", f"✅ {message}\n\nПапка: {backup_path}") else: QMessageBox.warning(self, "Ошибка", message) def restore_backup(self): from PyQt6.QtWidgets import QFileDialog from core.cache_manager import CacheManager from utils.file_utils import get_protocols_folder import zipfile import base64 zip_path, _ = QFileDialog.getOpenFileName( self, "Выберите ZIP-архив для восстановления", self.settings.get("last_backup_path", os.path.expanduser("~/Documents")), "ZIP архивы (*.zip)" ) if not zip_path: return with zipfile.ZipFile(zip_path, 'r') as zf: archived_files = [f for f in zf.namelist() if f.endswith('.enc')] if not archived_files: QMessageBox.warning(self, "Ошибка", "В архиве нет протоколов.") return # Проверяем наличие ключа в архиве old_key = None with zipfile.ZipFile(zip_path, 'r') as zf: if 'encryption_key.txt' in zf.namelist(): key_b64 = zf.read('encryption_key.txt').decode('ascii') old_key = base64.b64decode(key_b64) target_root = os.path.dirname(get_protocols_folder()) conflicts = [f for f in archived_files if os.path.exists(os.path.join(target_root, f))] if not conflicts: self._do_restore_simple(zip_path, target_root, archived_files, old_key) return # Конфликты — спрашиваем with zipfile.ZipFile(zip_path, 'r') as zf: restored = 0 skipped = 0 replace_all = False skip_all = False for filename in archived_files: target_path = os.path.join(target_root, filename) if not os.path.exists(target_path): zf.extract(filename, target_root) restored += 1 continue if replace_all: zf.extract(filename, target_root) restored += 1 continue if skip_all: skipped += 1 continue msg = QMessageBox(self) msg.setWindowTitle("Заменить файл?") msg.setText(f"Файл '{filename}' уже существует.\n\nЗаменить его?") btn_yes = msg.addButton("Да", QMessageBox.ButtonRole.YesRole) btn_yes_all = msg.addButton("Да для всех", QMessageBox.ButtonRole.YesRole) btn_no = msg.addButton("Нет", QMessageBox.ButtonRole.NoRole) btn_no_all = msg.addButton("Нет для всех", QMessageBox.ButtonRole.NoRole) btn_cancel = msg.addButton("Отмена", QMessageBox.ButtonRole.RejectRole) msg.exec() clicked = msg.clickedButton() if clicked == btn_cancel: break elif clicked == btn_yes_all: replace_all = True zf.extract(filename, target_root) restored += 1 elif clicked == btn_no_all: skip_all = True skipped += 1 elif clicked == btn_yes: zf.extract(filename, target_root) restored += 1 else: skipped += 1 # Восстанавливаем ключ if old_key: self.protocol_manager.set_encryption_key(old_key) from core.auth_manager import AuthManager auth = AuthManager() auth.set_encryption_key(old_key) # Обновляем кэш и помечаем восстановленные протоколы cache = CacheManager() cache.sync(self.protocol_manager) self._mark_restored_as_readonly(archived_files, target_root) QMessageBox.information(self, "Восстановление завершено", f"✅ Восстановлено: {restored}\n⏭️ Пропущено: {skipped}" "\n\nВосстановленные протоколы доступны только для чтения.") def _do_restore_simple(self, zip_path, target_root, archived_files, old_key): from core.backup import restore_backup from core.cache_manager import CacheManager success, message, count = restore_backup(zip_path, target_root) if success: if old_key: self.protocol_manager.set_encryption_key(old_key) from core.auth_manager import AuthManager auth = AuthManager() auth.set_encryption_key(old_key) cache = CacheManager() cache.sync(self.protocol_manager) self._mark_restored_as_readonly(archived_files, target_root) QMessageBox.information(self, "Восстановление завершено", f"✅ {message}\n\nВосстановленные протоколы доступны только для чтения.") else: QMessageBox.warning(self, "Ошибка", message) def _mark_restored_as_readonly(self, archived_files, target_root): """Помечает восстановленные протоколы как [Восстановлен] (readonly для всех)""" from core.cache_manager import CacheManager cache = CacheManager() for filename in archived_files: target_path = os.path.normpath(os.path.join(target_root, filename)) if os.path.exists(target_path): info = cache.get(target_path) if info: info['doctor_name'] = '[Восстановлен]' cache.add(target_path, info) # ========== СПРАВКА ========== def show_guide(self): from resource_helper import resource_path guide_path = resource_path("guide.html") if not os.path.exists(guide_path): QMessageBox.warning(self, "Ошибка", f"Файл руководства не найден:\n{guide_path}") return dialog = QDialog(self) dialog.setWindowTitle("Руководство пользователя") dialog.setGeometry(200, 200, 950, 750) layout = QVBoxLayout() browser = QTextBrowser() browser.setOpenExternalLinks(True) with open(guide_path, 'r', encoding='utf-8') as f: browser.setHtml(f.read()) layout.addWidget(browser) close_btn = QPushButton("Закрыть") close_btn.clicked.connect(dialog.close) layout.addWidget(close_btn) dialog.setLayout(layout) dialog.exec() def show_about(self): import webbrowser msg = QMessageBox(self) msg.setWindowTitle("О программе") msg.setText( "<h3>Протокол инструментального исследования</h3>" "<p><b>Версия:</b> 2.0</p>" "<p><b>Автор:</b> Шваб Олег Станиславович</p>" "<p><b>Год выпуска:</b> 2026</p>" "<p><b>Лицензия:</b> Бесплатное программное обеспечение</p>" "<p>Программа для создания, редактирования и печати протоколов инструментальных исследований.</p>" "<p>© 2026</p>" ) btn_support = msg.addButton("Поддержать автора", QMessageBox.ButtonRole.ActionRole) btn_ok = msg.addButton("OK", QMessageBox.ButtonRole.AcceptRole) msg.exec() if msg.clickedButton() == btn_support: webbrowser.open("https://pay.cloudtips.ru/p/3ae2fae8") def show_feedback(self): from core.feedback import FeedbackSender dialog = QDialog(self) dialog.setWindowTitle("✉️ Оставить отзыв") dialog.setGeometry(300, 300, 500, 350) layout = QVBoxLayout() layout.addWidget(QLabel("Расскажите о своём опыте.\nОтзыв будет отправлен разработчику.")) text_edit = QTextEdit() text_edit.setPlaceholderText("Введите ваш отзыв...") layout.addWidget(text_edit) send_btn = QPushButton("📨 Отправить") send_btn.clicked.connect(lambda: self._send_feedback(text_edit.toPlainText(), dialog)) layout.addWidget(send_btn) dialog.setLayout(layout) dialog.exec() def _send_feedback(self, text, dialog): from core.feedback import FeedbackSender if not text.strip(): QMessageBox.warning(dialog, "Ошибка", "Введите текст отзыва.") return sender = FeedbackSender() success, message = sender.send_feedback(text, version="2.0") if success: QMessageBox.information(dialog, "Успешно", message) dialog.close() else: QMessageBox.critical(dialog, "Ошибка", f"{message}\n\nПопробуйте позже или напишите на feedback-osprog@yandex.ru") # ========== НАВИГАЦИЯ ========== def go_back(self): self.close() def closeEvent(self, event): if self._check_unsaved(): self.settings.save_settings() if not self.readonly: from ui.main_window import MainWindow for widget in QApplication.topLevelWidgets(): if isinstance(widget, MainWindow): widget.show() break event.accept() else: event.ignore() def _check_unsaved(self): """Проверяет несохранённые изменения. Возвращает True если можно закрыть.""" if self.readonly: return True has_text = (self.description_editor.toPlainText().strip() or self.conclusion_editor.toPlainText().strip()) if not self.current_filepath and not has_text: return True if self.current_filepath: saved_desc, saved_concl, _ = self.protocol_manager.load_protocol_text(self.current_filepath) current_desc = self.description_editor.toPlainText().strip() current_concl = self.conclusion_editor.toPlainText().strip() if current_desc == saved_desc.strip() and current_concl == saved_concl.strip(): return True msg = QMessageBox(self) msg.setWindowTitle("Несохранённые изменения") msg.setText("Есть несохранённые изменения.") msg.setInformativeText("Сохранить протокол перед закрытием?") btn_save = msg.addButton("Да", QMessageBox.ButtonRole.AcceptRole) btn_no = msg.addButton("Нет", QMessageBox.ButtonRole.DestructiveRole) btn_cancel = msg.addButton("Отмена", QMessageBox.ButtonRole.RejectRole) msg.exec() clicked = msg.clickedButton() if clicked == btn_save: self.save_protocol() if self.current_filepath: return True else: return False elif clicked == btn_no: return True else: return False def _validate_exam_date(self): """Проверяет дату исследования""" text = self.exam_date.text().strip() if not text or len(text) < 10: self.exam_date.setStyleSheet("font-size: 14px; padding: 5px; width: 120px;") return try: day = int(text[:2]) month = int(text[3:5]) year = int(text[6:10]) from datetime import datetime exam_date = datetime(year, month, day) if exam_date > datetime.now(): raise ValueError except: self.exam_date.setStyleSheet("font-size: 14px; padding: 5px; width: 120px; background-color: #fadbd8; border: 1px solid #e74c3c;") QMessageBox.warning(self, "Неверная дата", "Введите корректную дату.") return self.exam_date.setStyleSheet("font-size: 14px; padding: 5px; width: 120px;") def _validate_exam_time(self): """Проверяет время исследования""" text = self.exam_time.text().strip() if not text or len(text) < 5: self.exam_time.setStyleSheet("font-size: 14px; padding: 5px; width: 80px;") return try: hour = int(text[:2]) minute = int(text[3:5]) if hour > 23 or minute > 59: raise ValueError except: self.exam_time.setStyleSheet("font-size: 14px; padding: 5px; width: 80px; background-color: #fadbd8; border: 1px solid #e74c3c;") QMessageBox.warning(self, "Неверное время", "Введите корректное время (00:00–23:59).") return self.exam_time.setStyleSheet("font-size: 14px; padding: 5px; width: 80px;") def _get_protocol_full_name(self, research_type): """Возвращает полное название для протокола: 'ультразвукового', 'рентгенологического' и т.д.""" from ui.main_window import PROTOCOL_NAMES if research_type in PROTOCOL_NAMES: return PROTOCOL_NAMES[research_type] adj_dict = self.settings.get("protocol_adjectives", {}) return adj_dict.get(research_type, research_type.lower())