# -*- coding: utf-8 -*- from flask import Flask, request, jsonify, Response, send_file, render_template_string import json from datetime import datetime import os import threading import time import logging from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError, HFValidationError from dotenv import load_dotenv import io load_dotenv() app = Flask(__name__) PATIENTS_DB = 'patients.json' PROTOCOLS_DB = 'protocols.json' CONTROL_DB = 'control.json' REPO_ID = "Kgshop/Medcentr" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') if not HF_TOKEN_WRITE: logging.warning("Токен HF_TOKEN (для записи) не установлен. Резервное копирование на Hugging Face не будет работать.") if not HF_TOKEN_READ: logging.warning("Токен HF_TOKEN_READ (для чтения) не установлен. Скачивание с Hugging Face не будет работать.") db_lock = threading.Lock() protocolDefinitions = { 'УЗИ внутренних органов': { 'category': 'Внутренних органов', 'fields': [ { 'name': 'protocol_type_header', 'label': 'Выберите протокол из списка:', 'type': 'header' }, { 'name': 'protocol_selection_note', 'label': 'Используйте кнопки ниже для выбора конкретного протокола УЗИ.', 'type': 'note' } ] }, 'УЗИ печени': { 'category': 'Печень', 'fields': [ { 'name': 'liver_label', 'label': 'Печень:', 'type': 'header'}, { 'name': 'liver_size_increase', 'label': 'Размеры', 'type': 'select', 'options': ['Не увеличена', 'Увеличена']}, { 'name': 'liver_right_lobe_kvr', 'label': 'Правая доля КВР (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'liver_left_lobe_size', 'label': 'Левая доля ПЗР (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'liver_contours', 'label': 'Контуры', 'type': 'select', 'options': ['Ровные', 'Неровные', 'Заострен', 'Закруглен']}, { 'name': 'liver_edge', 'label': 'Край', 'type': 'select', 'options': ['Ровный', 'Неровный']}, { 'name': 'liver_structure', 'label': 'Эхоструктура', 'type': 'select', 'options': ['Однородная', 'Неоднородная', 'Крупнозернистая', 'Мелкозернистая', 'Среднезернистая']}, { 'name': 'liver_echogenicity', 'label': 'Эхоплотность', 'type': 'select', 'options': ['Средняя', 'Повышена', 'Снижена']}, { 'name': 'liver_vessels', 'label': 'Сосудистый рисунок', 'type': 'select', 'options': ['Не изменен', 'Обеднен', 'Усилен']}, { 'name': 'liver_portal_vein', 'label': 'V. Portae (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'liver_hepatic_veins', 'label': 'Печеночные вены', 'type': 'select', 'options': ['Не изменены / сужены', 'Расширены']}, { 'name': 'liver_intrahepatic_ducts', 'label': 'Внутрипеченочные протоки', 'type': 'select', 'options': ['Не расширены', 'Расширены']}, { 'name': 'liver_lesions', 'label': 'Очаговые образования', 'type': 'textarea', 'placeholder': 'Описание, размеры...'}, { 'name': 'conclusion', 'label': 'Заключение', 'type': 'textarea', 'placeholder': 'Например: Эхо-признаки гепатоза.'} ] }, 'УЗИ желчного пузыря': { 'category': 'Желч. пузырь', 'fields': [ { 'name': 'gallbladder_label', 'label': 'Желчный пузырь:', 'type': 'header'}, { 'name': 'gallbladder_shape', 'label': 'Форма', 'type': 'select', 'options': ['Овоидная', 'Обычная', 'Изменена (перегиб и т.д.)']}, { 'name': 'gallbladder_size', 'label': 'Размеры (мм)', 'type': 'text', 'placeholder': 'Напр: 67x36'}, { 'name': 'gallbladder_wall', 'label': 'Стенка (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'gallbladder_contents', 'label': 'Содержимое', 'type': 'select', 'options': ['Гомогенное', 'Негомогенное', 'С осадком']}, { 'name': 'gallbladder_stones', 'label': 'Конкременты', 'type': 'select', 'options': ['Нет', 'Есть (описание)']}, { 'name': 'gallbladder_stones_desc', 'label': 'Описание конкрементов', 'type': 'textarea', 'condition': { 'field': 'gallbladder_stones', 'value': 'Есть (описание)'}}, { 'name': 'common_bile_duct', 'label': 'Общий желчный проток (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'conclusion', 'label': 'Заключение', 'type': 'textarea', 'placeholder': 'Например: ЖКБ. Хронический холецистит.'} ] }, 'УЗИ печени и желчного пузыря': { 'category': 'Печень + жел. пузырь', 'fields': [ { 'name': 'liver_label', 'label': 'Печень:', 'type': 'header'}, { 'name': 'liver_size_increase', 'label': 'Размеры', 'type': 'select', 'options': ['Не увеличена', 'Увеличена']}, { 'name': 'liver_right_lobe_kvr', 'label': 'Правая доля КВР (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'liver_left_lobe_size', 'label': 'Левая доля ПЗР (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'liver_contours', 'label': 'Контуры', 'type': 'select', 'options': ['Ровные', 'Неровные', 'Заострен', 'Закруглен']}, { 'name': 'liver_edge', 'label': 'Край', 'type': 'select', 'options': ['Ровный', 'Неровный']}, { 'name': 'liver_structure', 'label': 'Эхоструктура', 'type': 'select', 'options': ['Однородная', 'Неоднородная', 'Крупнозернистая', 'Мелкозернистая', 'Среднезернистая']}, { 'name': 'liver_echogenicity', 'label': 'Эхоплотность', 'type': 'select', 'options': ['Средняя', 'Повышена', 'Снижена']}, { 'name': 'liver_vessels', 'label': 'Сосудистый рисунок', 'type': 'select', 'options': ['Не изменен', 'Обеднен', 'Усилен']}, { 'name': 'liver_portal_vein', 'label': 'V. Portae (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'liver_hepatic_veins', 'label': 'Печеночные вены', 'type': 'select', 'options': ['Не изменены / сужены', 'Расширены']}, { 'name': 'liver_intrahepatic_ducts', 'label': 'Внутрипеченочные протоки', 'type': 'select', 'options': ['Не расширены', 'Расширены']}, { 'name': 'liver_lesions', 'label': 'Очаговые образования', 'type': 'textarea', 'placeholder': 'Описание, размеры...'}, { 'name': 'gallbladder_label', 'label': 'Желчный пузырь:', 'type': 'header'}, { 'name': 'gallbladder_shape', 'label': 'Форма', 'type': 'select', 'options': ['Овоидная', 'Обычная', 'Изменена (перегиб и т.д.)']}, { 'name': 'gallbladder_size', 'label': 'Размеры (мм)', 'type': 'text', 'placeholder': 'Напр: 67x36'}, { 'name': 'gallbladder_wall', 'label': 'Стенка (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'gallbladder_contents', 'label': 'Содержимое', 'type': 'select', 'options': ['Гомогенное', 'Негомогенное', 'С осадком']}, { 'name': 'gallbladder_stones', 'label': 'Конкременты', 'type': 'select', 'options': ['Нет', 'Есть (описание)']}, { 'name': 'gallbladder_stones_desc', 'label': 'Описание конкрементов', 'type': 'textarea', 'condition': { 'field': 'gallbladder_stones', 'value': 'Есть (описание)'}}, { 'name': 'common_bile_duct', 'label': 'Общий желчный проток (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'conclusion', 'label': 'Заключение', 'type': 'textarea', 'placeholder': 'Например: ЖКБ, Гепатоз.'} ] }, 'УЗИ поджелудочной железы': { 'category': 'Поджелудочная железа', 'fields': [ { 'name': 'pancreas_label', 'label': 'Поджелудочная железа:', 'type': 'header'}, { 'name': 'pancreas_visualization', 'label': 'Визуализация', 'type': 'select', 'options': ['Хорошо / удовлетворительно', 'Затруднена', 'Не визуализируется']}, { 'name': 'pancreas_head', 'label': 'Головка (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'pancreas_body', 'label': 'Тело (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'pancreas_tail', 'label': 'Хвост (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'pancreas_duct', 'label': 'Вирсунгов проток', 'type': 'select', 'options': ['Не расширен', 'Расширен (до мм)']}, { 'name': 'pancreas_duct_size', 'label': 'Расширение Вирсунгова протока (мм)', 'type': 'number', 'step': '0.1', 'condition': { 'field': 'pancreas_duct', 'value': 'Расширен (до мм)'}}, { 'name': 'pancreas_contours', 'label': 'Контуры', 'type': 'select', 'options': ['Четкие / ровные', 'Нечеткие / неровные']}, { 'name': 'pancreas_structure', 'label': 'Эхоструктура', 'type': 'select', 'options': ['Однородная / неоднородная', 'С включениями']}, { 'name': 'pancreas_echogenicity', 'label': 'Эхогенность', 'type': 'select', 'options': ['Обычная / Не изменена', 'Повышена', 'Снижена']}, { 'name': 'pancreas_lesions', 'label': 'Очаговые образования', 'type': 'textarea', 'placeholder': 'Описание...'}, { 'name': 'conclusion', 'label': 'Заключение', 'type': 'textarea', 'placeholder': 'Например: Хронический панкреатит.'} ] }, 'УЗИ почек': { 'category': 'УЗИ почек', 'fields': [ { 'name': 'right_kidney_label', 'label': 'Правая почка:', 'type': 'header'}, { 'name': 'right_kidney_shape', 'label': 'Форма', 'type': 'select', 'options': ['Бобовидная', 'Обычная', 'Изменена']}, { 'name': 'right_kidney_contours', 'label': 'Контуры', 'type': 'select', 'options': ['Ровные, четкие', 'Неровные', 'Нечеткие']}, { 'name': 'right_kidney_size', 'label': 'Размер ДхШхТ (мм)', 'type': 'text', 'placeholder': 'Напр: 106х40.31х50.89'}, { 'name': 'right_kidney_volume', 'label': 'Общий объем (см3)', 'type': 'number', 'step': '0.1'}, { 'name': 'right_kidney_parenchyma', 'label': 'Паренхима (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'right_kidney_sinus', 'label': 'Почечный синус', 'type': 'select', 'options': ['Уплотнен', 'Не уплотнен', 'Деформирован', 'Не деформирован']}, { 'name': 'right_kidney_chlk', 'label': 'ЧЛК', 'type': 'select', 'options': ['Не расширен', 'Расширен']}, { 'name': 'right_kidney_chlk_size', 'label': 'Расширение ЧЛК до (мм)', 'type': 'number', 'step': '0.1', 'condition': { 'field': 'right_kidney_chlk', 'value': 'Расширен'}}, { 'name': 'right_kidney_urine_flow', 'label': 'Отток мочи', 'type': 'select', 'options': ['Не нарушен', 'Затруднен']}, { 'name': 'right_kidney_features', 'label': 'Особенности (конкр., кисты, образ.)', 'type': 'textarea', 'placeholder': 'Описание...' }, { 'name': 'left_kidney_label', 'label': 'Левая почка:', 'type': 'header'}, { 'name': 'left_kidney_shape', 'label': 'Форма', 'type': 'select', 'options': ['Бобовидная', 'Обычная', 'Изменена']}, { 'name': 'left_kidney_contours', 'label': 'Контуры', 'type': 'select', 'options': ['Ровные, четкие', 'Неровные', 'Нечеткие']}, { 'name': 'left_kidney_size', 'label': 'Размер ДхШхТ (мм)', 'type': 'text', 'placeholder': 'Напр: 101.70х42.14х50.73'}, { 'name': 'left_kidney_volume', 'label': 'Общий объем (см3)', 'type': 'number', 'step': '0.1'}, { 'name': 'left_kidney_parenchyma', 'label': 'Паренхима (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'left_kidney_sinus', 'label': 'Почечный синус', 'type': 'select', 'options': ['Уплотнен', 'Не уплотнен', 'Деформирован', 'Не деформирован']}, { 'name': 'left_kidney_chlk', 'label': 'ЧЛК', 'type': 'select', 'options': ['Не расширен', 'Расширен']}, { 'name': 'left_kidney_chlk_size', 'label': 'Расширение ЧЛК до (мм)', 'type': 'number', 'step': '0.1', 'condition': { 'field': 'left_kidney_chlk', 'value': 'Расширен'}}, { 'name': 'left_kidney_urine_flow', 'label': 'Отток мочи', 'type': 'select', 'options': ['Не нарушен', 'Затруднен']}, { 'name': 'left_kidney_features', 'label': 'Особенности (конкр., кисты, образ.)', 'type': 'textarea', 'placeholder': 'Описание...' }, { 'name': 'both_kidneys_label', 'label': 'Общее:', 'type': 'header'}, { 'name': 'echostructure_notes', 'label': 'Особенности эхоструктуры', 'type': 'textarea', 'placeholder': 'Напр: В обеих почках определяются гиперэхогенной структуры солевые включения...'}, { 'name': 'conclusion', 'label': 'Заключение', 'type': 'textarea', 'placeholder': 'Например: Хронический пиелонефрит обеих почек. Микролитиаз почек.'} ] }, 'УЗИ щитовидной железы':{ 'category': 'Щитовидная железа', 'fields': [ { 'name': 'thyroid_structure_label', 'label': 'Анатомическое строение:', 'type': 'header' }, { 'name': 'thyroid_structure_desc', 'label': 'Описание железы', 'type': 'text', 'default': 'Щитовидная железа представлена двумя долями, соединенными перешейком'}, { 'name': 'isthmus_thickness', 'label': 'Толщина перешейка (мм)', 'type': 'number', 'step': '0.01' }, { 'name': 'right_lobe_label', 'label': 'Правая доля:', 'type': 'header' }, { 'name': 'right_lobe_length', 'label': 'Длина (мм)', 'type': 'number', 'step': '0.01' }, { 'name': 'right_lobe_width', 'label': 'Ширина (мм)', 'type': 'number', 'step': '0.01' }, { 'name': 'right_lobe_depth', 'label': 'Глубина (мм)', 'type': 'number', 'step': '0.01' }, { 'name': 'right_lobe_volume', 'label': 'Объем (см3)', 'type': 'number', 'step': '0.01' }, { 'name': 'left_lobe_label', 'label': 'Левая доля:', 'type': 'header' }, { 'name': 'left_lobe_length', 'label': 'Длина (мм)', 'type': 'number', 'step': '0.01' }, { 'name': 'left_lobe_width', 'label': 'Ширина (мм)', 'type': 'number', 'step': '0.01' }, { 'name': 'left_lobe_depth', 'label': 'Глубина (мм)', 'type': 'number', 'step': '0.01' }, { 'name': 'left_lobe_volume', 'label': 'Объем (см3)', 'type': 'number', 'step': '0.01' }, { 'name': 'total_volume_label', 'label': 'Общий объем:', 'type': 'header' }, { 'name': 'total_volume', 'label': 'Общий объем (см3)', 'type': 'number', 'step': '0.01' }, { 'name': 'total_volume_norm', 'label': 'Норма объема', 'type': 'text', 'default': 'в норме до 18 см3' }, { 'name': 'vascularization', 'label': 'Васкуляризация при ЦДК', 'type': 'select', 'options': ['Обычная', 'Усилена', 'Снижена', 'Средней степени'] }, { 'name': 'lymph_nodes', 'label': 'Лимфатические узлы шеи', 'type': 'textarea', 'placeholder': 'Описание узлов I-VII уровней' }, { 'name': 'conclusion', 'label': 'Заключение (TIRADS)', 'type': 'textarea', 'placeholder': 'Напр: TIRADS-0. Без признаков патологии.' }, { 'name': 'recommendations', 'label': 'Рекомендации', 'type': 'textarea', 'placeholder': 'Напр: Контроль гормонов щитовидной железы. Консультация эндокринолога.' } ] }, 'УЗИ селезёнки': { 'category': 'Селезёнка', 'fields': [ { 'name': 'spleen_label', 'label': 'Селезенка:', 'type': 'header'}, { 'name': 'spleen_size_increase', 'label': 'Размеры', 'type': 'select', 'options': ['Не увеличена', 'Увеличена']}, { 'name': 'spleen_size', 'label': 'Размеры (мм)', 'type': 'text', 'placeholder': 'Напр: 112x56'}, { 'name': 'spleen_contours', 'label': 'Контуры', 'type': 'select', 'options': ['Ровные / неровные']}, { 'name': 'spleen_edge', 'label': 'Край', 'type': 'select', 'options': ['Не изменен / закруглен']}, { 'name': 'spleen_structure', 'label': 'Эхоструктура', 'type': 'select', 'options': ['Однородная / неоднородная']}, { 'name': 'spleen_echogenicity', 'label': 'Эхогенность', 'type': 'select', 'options': ['Обычная / Не изменена', 'Повышена', 'Снижена']}, { 'name': 'spleen_lesions', 'label': 'Очаговые образования', 'type': 'textarea', 'placeholder': 'Описание...'}, { 'name': 'conclusion', 'label': 'Заключение', 'type': 'textarea', 'placeholder': 'Например: Эхо-признаки спленомегалии.'} ] }, 'УЗИ матки': { 'category': 'Матка', 'fields': [ { 'name': 'uterus_position', 'label': 'Матка (положение)', 'type': 'select', 'options': ['anteversio', 'retroversio', 'другое'] }, { 'name': 'uterus_size', 'label': 'Размеры тела матки (ДxШxВ, мм)', 'type': 'text', 'placeholder': 'Напр: 60x46x51' }, { 'name': 'uterus_structure', 'label': 'Структура миометрия', 'type': 'select', 'options': ['Однородная', 'Неоднородная'] }, { 'name': 'uterus_contours', 'label': 'Контуры матки', 'type': 'select', 'options': ['Четкие, ровные', 'Нечеткие', 'Неровные'] }, { 'name': 'myometrium_echo', 'label': 'Эхоструктура миометрия (особенности)', 'type': 'textarea', 'placeholder': 'Описание изменений, узлов' }, { 'name': 'endometrium_thickness', 'label': 'Эндометрий: М-эхо (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'endometrium_structure', 'label': 'Эндометрий: Структура', 'type': 'textarea', 'placeholder': 'Описание структуры, соответствие фазе цикла' }, { 'name': 'uterus_cavity', 'label': 'Полость матки', 'type': 'select', 'options': ['Не расширена', 'Расширена'] }, { 'name': 'conclusion', 'label': 'Заключение', 'type': 'textarea', 'placeholder': 'Например: Миома матки. Гиперплазия эндометрия...' } ] }, 'УЗИ молочных желез': { 'category': 'Молочная железа', 'fields': [ { 'name': 'mammary_type', 'label': 'Тип строения молочных желез', 'type': 'select', 'options': ['Железистый', 'Жировой', 'Смешанный'] }, { 'name': 'mammary_symmetry', 'label': 'Молочные железы', 'type': 'select', 'options': ['Симметричные', 'Асимметричные'] }, { 'name': 'nipple_areola_skin', 'label': 'Сосково-премаммарная зона и кожа', 'type': 'select', 'options': ['Не изменена', 'Изменена'] }, { 'name': 'nipple_areola_skin_desc', 'label': 'Описание изменений зоны/кожи', 'type': 'textarea', 'condition': { 'field': 'nipple_areola_skin', 'value': 'Изменена'}}, { 'name': 'right_breast_header', 'label': 'Правая молочная железа:', 'type': 'header'}, { 'name': 'right_breast_visual', 'label': 'Визуализация', 'type': 'select', 'options': ['Удовлетворительная', 'Затруднена']}, { 'name': 'right_breast_tissue_dist', 'label': 'Распределение тканей', 'type': 'textarea', 'placeholder': 'Жировая ткань, железистая ткань'}, { 'name': 'right_breast_fgk_mm', 'label': 'Толщина ФГК (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'right_breast_hyperplasia', 'label': 'Умеренная гиперплазия', 'type': 'select', 'options': ['Да', 'Нет']}, { 'name': 'right_breast_ducts', 'label': 'Млечные протоки', 'type': 'select', 'options': ['Не расширены', 'Расширены']}, { 'name': 'right_breast_ducts_size', 'label': 'Диаметр расширенных протоков (мм)', 'type': 'number', 'step': '0.1', 'condition': { 'field': 'right_breast_ducts', 'value': 'Расширены'}}, { 'name': 'right_breast_retromammary', 'label': 'Ретромаммарная клетчатка', 'type': 'select', 'options': ['Не изменена', 'Изменена']}, { 'name': 'right_breast_post_nipple_visual', 'label': 'Визуализация позадисосковой области', 'type': 'select', 'options': ['Хорошая', 'Затруднена']}, { 'name': 'right_breast_vascularization', 'label': 'Васкуляризация', 'type': 'select', 'options': ['Сохранена', 'Усилена', 'Снижена']}, { 'name': 'right_breast_quadrants', 'label': 'Изменения по квадрантам', 'type': 'textarea', 'placeholder': 'Описание кистозных и др. изменений, их размеры'}, { 'name': 'left_breast_header', 'label': 'Левая молочная железа:', 'type': 'header'}, { 'name': 'left_breast_visual', 'label': 'Визуализация', 'type': 'select', 'options': ['Удовлетворительная', 'Затруднена']}, { 'name': 'left_breast_tissue_dist', 'label': 'Распределение тканей', 'type': 'textarea', 'placeholder': 'Жировая ткань, железистая ткань'}, { 'name': 'left_breast_fgk_mm', 'label': 'Толщина ФГК (мм)', 'type': 'number', 'step': '0.1'}, { 'name': 'left_breast_hyperplasia', 'label': 'Умеренная гиперплазия', 'type': 'select', 'options': ['Да', 'Нет']}, { 'name': 'left_breast_ducts', 'label': 'Млечные протоки', 'type': 'select', 'options': ['Не расширены', 'Расширены']}, { 'name': 'left_breast_ducts_size', 'label': 'Диаметр расширенных протоков (мм)', 'type': 'number', 'step': '0.1', 'condition': { 'field': 'left_breast_ducts', 'value': 'Расширены'}}, { 'name': 'left_breast_retromammary', 'label': 'Ретромаммарная клетчатка', 'type': 'select', 'options': ['Не изменена', 'Изменена']}, { 'name': 'left_breast_post_nipple_visual', 'label': 'Визуализация позадисосковой области', 'type': 'select', 'options': ['Хорошая', 'Затруднена']}, { 'name': 'left_breast_vascularization', 'label': 'Васкуляризация', 'type': 'select', 'options': ['Сохранена', 'Усилена', 'Снижена']}, { 'name': 'left_breast_quadrants', 'label': 'Изменения по квадрантам', 'type': 'textarea', 'placeholder': 'Описание кистозных и др. изменений, их размеры'}, { 'name': 'general_tissue_header', 'label': 'Общая характеристика тканей:', 'type': 'header'}, { 'name': 'dominant_tissue', 'label': 'Преимущественное преобладание ткани', 'type': 'select', 'options': ['Железистой', 'Жировой', 'Фиброзной']}, { 'name': 'tissue_changes', 'label': 'Изменения структуры ткани', 'type': 'textarea', 'placeholder': 'Напр: Диффузные изменения железистой ткани...'}, { 'name': 'focal_lesions', 'label': 'Очаговые образования', 'type': 'textarea', 'placeholder': 'Локализация, размеры, контуры, эхогенность'}, { 'name': 'skin_subcutaneous_fat', 'label': 'Кожа и подкожно-жировой слой', 'type': 'select', 'options': ['Не утолщены, дифференциация сохранена', 'Утолщены', 'Дифференциация нарушена']}, { 'name': 'fibroglandular_tissue', 'label': 'Фиброгландулярная ткань', 'type': 'textarea', 'placeholder': 'Напр: Очаги умеренно пониженной эхогенности... Гиперплазия...'}, { 'name': 'tissue_differentiation', 'label': 'Дифференциация тканей', 'type': 'select', 'options': ['Четкая', 'Нечеткая', 'Нарушена']}, { 'name': 'lymph_nodes_header', 'label': 'Регионарные лимфоузлы:', 'type': 'header'}, { 'name': 'lymph_nodes_right', 'label': 'Справа в подмышечной области', 'type': 'textarea', 'placeholder': 'Визуализация, размеры, структура'}, { 'name': 'lymph_nodes_left', 'label': 'Слева в подмышечной области', 'type': 'textarea', 'placeholder': 'Визуализация, размеры, структура'}, { 'name': 'conclusion_header', 'label': 'Заключение:', 'type': 'header'}, { 'name': 'birads_right', 'label': 'BIRADS правой молочной железы', 'type': 'select', 'options': ['0', '1', '2', '3', '4a', '4b', '4c', '5', '6'] }, { 'name': 'birads_left', 'label': 'BIRADS левой молочной железы', 'type': 'select', 'options': ['0', '1', '2', '3', '4a', '4b', '4c', '5', '6'] }, { 'name': 'conclusion_text', 'label': 'Описание заключения', 'type': 'textarea', 'placeholder': 'Напр: Фиброзно-кистозная мастопатия обеих молочных желез.' }, { 'name': 'recommendations', 'label': 'Рекомендовано', 'type': 'textarea', 'placeholder': 'Напр: УЗИ в динамике.' }, ] }, 'УЗИ лонного сочленения': { 'category': 'Лонное сочленение', 'fields': [ { 'name': 'symphysis_surface', 'label': 'Структура поверхности лонных костей', 'type': 'select', 'options': ['Обычная', 'Изменена'] }, { 'name': 'symphysis_surface_desc', 'label': 'Описание изменений поверхности', 'type': 'textarea', 'condition': { 'field': 'symphysis_surface', 'value': 'Изменена'} }, { 'name': 'symphysis_disc_echo', 'label': 'Эхогенность диска симфиза', 'type': 'select', 'options': ['Однородная', 'Неоднородная'] }, { 'name': 'superior_pubic_ligament', 'label': 'Верхняя лонная связка', 'type': 'select', 'options': ['Обычная', 'Изменена'] }, { 'name': 'superior_pubic_ligament_desc', 'label': 'Описание изменений верхней связки', 'type': 'textarea', 'condition': { 'field': 'superior_pubic_ligament', 'value': 'Изменена'} }, { 'name': 'arcuate_pubic_ligament', 'label': 'Дугообразная лонная связка', 'type': 'select', 'options': ['Визуализируется', 'Не визуализируется'] }, { 'name': 'color_doppler_findings', 'label': 'В режиме ЦДК', 'type': 'select', 'options': ['Нормальная васкуляризация', 'Единичные цветные локусы', 'Изменения (описать)'] }, { 'name': 'color_doppler_desc', 'label': 'Описание изменений ЦДК', 'type': 'textarea', 'condition': { 'field': 'color_doppler_findings', 'value': 'Изменения (описать)'} }, { 'name': 'pubic_rami_alignment', 'label': 'Высота стояния ветвей лонных костей в покое', 'type': 'select', 'options': ['На одном уровне', 'Разница (мм)'] }, { 'name': 'pubic_rami_diff_mm', 'label': 'Разница высоты стояния (мм)', 'type': 'number', 'step': '0.1', 'condition': { 'field': 'pubic_rami_alignment', 'value': 'Разница (мм)'} }, { 'name': 'displacement_test', 'label': 'Проба на смещение', 'type': 'select', 'options': ['Изменение уровня минимально-симфиз состоятелен', 'Значительное смещение (мм)'] }, { 'name': 'displacement_mm', 'label': 'Величина смещения (мм)', 'type': 'number', 'step': '0.1', 'condition': { 'field': 'displacement_test', 'value': 'Значительное смещение (мм)'} }, { 'name': 'pubic_distance_mm', 'label': 'Расстояние лонных костей (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'pubic_distance_norm', 'label': 'Норма расстояния', 'type': 'text', 'default': 'N- до 10мм' }, { 'name': 'conclusion', 'label': 'УЗИ признаки', 'type': 'textarea', 'placeholder': 'Напр: Признаки симфизита.' }, { 'name': 'doctor_notes', 'label': 'Комментарий врача', 'type': 'textarea', 'placeholder': '(Оставьте комментарий, если необходимо)' }, ] }, 'УЗИ яичек и простаты': { 'category': 'Яичек и простаты', 'fields': [ { 'name': 'prostate_label', 'label': 'Предстательная железа:', 'type': 'header' }, { 'name': 'prostate_size', 'label': 'Размеры простаты (см3)', 'type': 'number', 'step': '0.1' }, { 'name': 'prostate_structure', 'label': 'Структура простаты', 'type': 'select', 'options': ['Однородная', 'Неоднородная'] }, { 'name': 'prostate_contours', 'label': 'Контуры простаты', 'type': 'select', 'options': ['Ровные', 'Неровные'] }, { 'name': 'prostate_echo', 'label': 'Эхогенность простаты', 'type': 'select', 'options': ['Обычная', 'Повышенная', 'Сниженная'] }, { 'name': 'sv_label', 'label': 'Семенные пузырьки:', 'type': 'header' }, { 'name': 'sv_structure', 'label': 'Структура семенных пузырьков', 'type': 'select', 'options': ['Не изменены', 'Изменены'] }, { 'name': 'sv_features', 'label': 'Особенности семенных пузырьков', 'type': 'textarea', 'placeholder': 'Описание особенностей' }, { 'name': 'testicles_label', 'label': 'Яички:', 'type': 'header' }, { 'name': 'right_testicle_size', 'label': 'Размеры правого яичка (мм)', 'type': 'text', 'placeholder': 'Напр: 40x30x25' }, { 'name': 'left_testicle_size', 'label': 'Размеры левого яичка (мм)', 'type': 'text', 'placeholder': 'Напр: 41x31x26' }, { 'name': 'testicles_structure', 'label': 'Структура яичек', 'type': 'select', 'options': ['Однородная', 'Неоднородная'] }, { 'name': 'epididymis_label', 'label': 'Придатки яичек:', 'type': 'header' }, { 'name': 'epididymis_structure', 'label': 'Структура придатков', 'type': 'select', 'options': ['Не изменены', 'Изменены'] }, { 'name': 'epididymis_features', 'label': 'Особенности придатков', 'type': 'textarea', 'placeholder': 'Описание особенностей' }, { 'name': 'scrotum_features', 'label': 'Особенности мошонки', 'type': 'textarea', 'placeholder': 'Напр: Гидроцеле, Варикоцеле' }, { 'name': 'conclusion', 'label': 'Заключение', 'type': 'textarea', 'placeholder': 'Например: Хронический простатит. Варикоцеле слева.' } ] }, 'УЗИ мочевого пузыря': { 'category': 'Мочевой пузырь', 'fields': [ { 'name': 'bladder_shape', 'label': 'Форма', 'type': 'select', 'options': ['Симметричной формы', 'Овальной формы', 'Неправильной формы'] }, { 'name': 'bladder_contours', 'label': 'Контуры', 'type': 'select', 'options': ['Ровные', 'Неровные', 'Четкие', 'Нечеткие'] }, { 'name': 'bladder_volume', 'label': 'Объем (мл)', 'type': 'number' }, { 'name': 'bladder_wall_thickness', 'label': 'Толщина стенки (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'bladder_inner_surface', 'label': 'Внутренняя поверхность', 'type': 'select', 'options': ['Гладкая', 'Не гладкая', 'С трабекулярностью'] }, { 'name': 'bladder_neck', 'label': 'Шейка', 'type': 'select', 'options': ['Формируется', 'Не формируется'] }, { 'name': 'bladder_lumen', 'label': 'Просвет', 'type': 'select', 'options': ['Свободный', 'С осадком', 'С включениями'] }, { 'name': 'residual_urine', 'label': 'Остаточной мочи', 'type': 'select', 'options': ['Нет', 'Есть'] }, { 'name': 'residual_urine_volume', 'label': 'Объем остаточной мочи (мл)', 'type': 'number', 'condition': { 'field': 'residual_urine', 'value': 'Есть' } }, { 'name': 'conclusion', 'label': 'Заключение', 'type': 'textarea', 'placeholder': 'Например: Эхоструктурных изменений мочевого пузыря не выявлено.'} ] }, 'УЗИ беременности (ранний срок)': { 'category': 'Беременность ранний срок', 'fields': [ { 'name': 'first_day_last_menstruation', 'label': 'Первый день последней менструации', 'type': 'date' }, { 'name': 'scan_type', 'label': 'Вид исследования', 'type': 'select', 'options': ['Трансвагинально', 'Трансабдоминально', 'Трансвагинально, трансабдоминально'] }, { 'name': 'fetal_egg_count', 'label': 'Плодное яйцо', 'type': 'select', 'options': ['1', '2', '3+'] }, { 'name': 'svd', 'label': 'СВД (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'ktr', 'label': 'КТР (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'yolk_sac', 'label': 'Желточный мешок (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'heartbeat', 'label': 'Сердцебиение', 'type': 'select', 'options': ['+', '-', 'не определяется'] }, { 'name': 'hypertonus_location', 'label': 'Участок гипертонуса', 'type': 'text', 'placeholder': 'Например: по задней стенке' }, { 'name': 'conclusion_weeks', 'label': 'Заключение: Беременность (недель)', 'type': 'number' }, { 'name': 'conclusion_days', 'label': 'Заключение: Беременность (дней)', 'type': 'number' }, { 'name': 'recommendations', 'label': 'Рекомендации', 'type': 'textarea', 'placeholder': 'Например: Консультация гинеколога. УЗИ скрининг...' } ] }, 'УЗИ беременности (1 скрининг 11-13+6 нед)': { 'category': 'I скрининг', 'fields': [ { 'name': 'gestational_age_source', 'label': 'Срок беременности по:', 'type': 'select', 'options': ['Дате ПДМ', 'УЗИ КТР', 'ЭКО'] }, { 'name': 'gestational_age_weeks', 'label': 'Срок на дату УЗИ (нед)', 'type': 'number' }, { 'name': 'gestational_age_days', 'label': 'Срок на дату УЗИ (+ дней)', 'type': 'number' }, { 'name': 'last_menstrual_period_date', 'label': 'Дата ПДМ', 'type': 'date' }, { 'name': 'scan_type', 'label': 'Вид исследования', 'type': 'select', 'options': ['Трансабдоминальный', 'Трансвагинальный', 'Комбинированный'] }, { 'name': 'fetal_count', 'label': 'Количество плодов', 'type': 'number', 'default': 1 }, { 'name': 'chorionicity', 'label': 'Хориальность (при многоплодии)', 'type': 'text', 'placeholder': 'Монохориальная, Бихориальная...' }, { 'name': 'amnionicity', 'label': 'Амниональность (при многоплодии)', 'type': 'text', 'placeholder': 'Моноамниотическая, Биамниотическая...' }, { 'name': 'fhr_bpm', 'label': 'ЧСС плода (уд/мин)', 'type': 'number' }, { 'name': 'ktr_mm', 'label': 'КТР (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'tvp_mm', 'label': 'ТВП (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'bpr_mm', 'label': 'БПР головы (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'og_mm', 'label': 'Окружность головы (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'oj_mm', 'label': 'Окружность живота (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'dbk_mm', 'label': 'Длина бедренной кости (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'nasal_bone', 'label': 'Кость носа', 'type': 'select', 'options': ['Визуализируется', 'Не визуализируется', 'Гипоплазия'] }, { 'name': 'nasal_bone_mm', 'label': 'Длина кости носа (мм)', 'type': 'number', 'step': '0.1', 'condition': { 'field': 'nasal_bone', 'value': 'Визуализируется'}}, { 'name': 'ductus_venosus_pi', 'label': 'Кровоток в венозном протоке (PI)', 'type': 'number', 'step': '0.01' }, { 'name': 'tricuspid_regurgitation', 'label': 'Трикуспидальная регургитация', 'type': 'select', 'options': ['Нет', 'Есть'] }, { 'name': 'anatomy_header', 'label': 'Анатомия плода:', 'type': 'header' }, { 'name': 'anatomy_head_skull', 'label': 'Головка / Кости черепа', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_brain_structures', 'label': 'Структуры головного мозга', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_face', 'label': 'Лицо (профиль, носогуб. треуг.)', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_spine', 'label': 'Позвоночник', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_neck_chest', 'label': 'Шея / Грудная клетка', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_heart', 'label': 'Сердце (4-кам. срез)', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_lungs', 'label': 'Легкие', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_abdominal_wall', 'label': 'Передняя брюшная стенка', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_stomach', 'label': 'Желудок', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_intestine', 'label': 'Кишечник', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_kidneys', 'label': 'Почки', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_bladder', 'label': 'Мочевой пузырь', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_upper_limbs', 'label': 'Верхние конечности (обе)', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_lower_limbs', 'label': 'Нижние конечности (обе)', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_details', 'label': 'Описание выявленных особенностей анатомии', 'type': 'textarea' }, { 'name': 'chorion_placenta_header', 'label': 'Хорион / Плацента:', 'type': 'header' }, { 'name': 'chorion_location', 'label': 'Локализация хориона', 'type': 'text', 'placeholder': 'Напр: По передней/задней стенке, дно...' }, { 'name': 'chorion_structure', 'label': 'Структура хориона', 'type': 'select', 'options': ['Не изменена', 'Изменена'] }, { 'name': 'chorion_structure_desc', 'label': 'Описание изменений структуры', 'type': 'textarea', 'condition': {'field': 'chorion_structure', 'value': 'Изменена'}}, { 'name': 'amniotic_fluid_header', 'label': 'Околоплодные воды:', 'type': 'header' }, { 'name': 'amniotic_fluid_volume', 'label': 'Количество', 'type': 'select', 'options': ['Нормальное', 'Увеличено', 'Уменьшено'] }, { 'name': 'amniotic_fluid_features', 'label': 'Особенности вод', 'type': 'textarea', 'placeholder': 'Напр: Прозрачные, с взвесью...' }, { 'name': 'other_features_header', 'label': 'Другие особенности:', 'type': 'header' }, { 'name': 'yolk_sac_visual', 'label': 'Желточный мешок', 'type': 'select', 'options': ['Визуализируется', 'Не визуализируется', 'Изменен'] }, { 'name': 'uterus_features', 'label': 'Особенности матки', 'type': 'textarea', 'placeholder': 'Напр: Миоматозные узлы (локализация, размеры)'}, { 'name': 'ovaries_features', 'label': 'Особенности яичников', 'type': 'textarea', 'placeholder': 'Напр: Киста желтого тела (размер)'}, { 'name': 'conclusion_header', 'label': 'Заключение:', 'type': 'header' }, { 'name': 'conclusion_pregnancy', 'label': 'Беременность (нед/дней)', 'type': 'text', 'placeholder': 'Напр: 12 недель 3 дня' }, { 'name': 'conclusion_features', 'label': 'Особенности', 'type': 'textarea', 'placeholder': 'Краткое описание основных находок или их отсутствия' }, { 'name': 'recommendations', 'label': 'Рекомендации', 'type': 'textarea', 'placeholder': 'Напр: Биохимический скрининг, консультация генетика...' } ] }, 'УЗИ беременности (2 скрининг 18-21 нед)': { 'category': 'II скрининг', 'fields': [ { 'name': 'gestational_age_source', 'label': 'Срок беременности по:', 'type': 'select', 'options': ['Дате ПДМ', '1 скринингу', 'ЭКО'] }, { 'name': 'gestational_age_weeks', 'label': 'Срок на дату УЗИ (нед)', 'type': 'number' }, { 'name': 'gestational_age_days', 'label': 'Срок на дату УЗИ (+ дней)', 'type': 'number' }, { 'name': 'last_menstrual_period_date', 'label': 'Дата ПДМ', 'type': 'date' }, { 'name': 'previous_screening_date', 'label': 'Дата 1 скрининга', 'type': 'date' }, { 'name': 'fetometry_header', 'label': 'Фетометрия:', 'type': 'header' }, { 'name': 'bpr_mm', 'label': 'БПР (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'og_mm', 'label': 'ОГ (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'oj_mm', 'label': 'ОЖ (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'dbk_mm', 'label': 'Длина бедренной кости (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'dpk_mm', 'label': 'Длина плечевой кости (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'estimated_fetal_weight_g', 'label': 'Предполагаемая масса плода (г)', 'type': 'number' }, { 'name': 'fetal_percentile', 'label': 'Процентиль массы плода', 'type': 'number' }, { 'name': 'anatomy_header', 'label': 'Анатомия плода:', 'type': 'header' }, { 'name': 'anatomy_head_skull', 'label': 'Кости черепа', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_brain_structures', 'label': 'Структуры гол. мозга (желудочки, мозжечок, цистерна)', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_face_profile', 'label': 'Лицо: профиль', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_face_nasolabial', 'label': 'Лицо: носогубный треугольник', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_face_orbits', 'label': 'Лицо: глазницы', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_spine', 'label': 'Позвоночник', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_lungs', 'label': 'Легкие', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_heart_chambers', 'label': 'Сердце: 4-камерный срез', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_heart_great_vessels', 'label': 'Сердце: срез через 3 сосуда', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_diaphragm', 'label': 'Диафрагма', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_stomach', 'label': 'Желудок', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_intestine', 'label': 'Кишечник', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_liver_gallbladder', 'label': 'Печень, Желчный пузырь', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_abdominal_wall', 'label': 'Передняя брюшная стенка', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_kidneys_renal_pelvis', 'label': 'Почки, лоханки', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_bladder', 'label': 'Мочевой пузырь', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_sex', 'label': 'Пол плода', 'type': 'select', 'options': ['Мужской', 'Женский', 'Не определен'] }, { 'name': 'anatomy_upper_limbs', 'label': 'Верхние конечности (кости, кисти)', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_lower_limbs', 'label': 'Нижние конечности (кости, стопы)', 'type': 'select', 'options': ['Норма', 'Патология', 'Не виз.'] }, { 'name': 'anatomy_details', 'label': 'Описание выявленных особенностей анатомии', 'type': 'textarea' }, { 'name': 'placenta_header', 'label': 'Плацента:', 'type': 'header' }, { 'name': 'placenta_location', 'label': 'Локализация', 'type': 'text', 'placeholder': 'Напр: По задней стенке, дно...' }, { 'name': 'placenta_edge_distance', 'label': 'Расстояние нижнего края от вн. зева (мм)', 'type': 'number', 'step': '1'}, { 'name': 'placenta_thickness_mm', 'label': 'Толщина (мм)', 'type': 'number', 'step': '1'}, { 'name': 'placenta_maturity_grade', 'label': 'Степень зрелости (по Grannum)', 'type': 'select', 'options': ['0', 'I', 'II', 'III']}, { 'name': 'placenta_structure', 'label': 'Структура', 'type': 'select', 'options': ['Однородная / без особенностей', 'Неоднородная (описание)']}, { 'name': 'placenta_structure_desc', 'label': 'Описание изменений структуры', 'type': 'textarea', 'condition': {'field': 'placenta_structure', 'value': 'Неоднородная (описание)'}}, { 'name': 'amniotic_fluid_header', 'label': 'Околоплодные воды:', 'type': 'header' }, { 'name': 'amniotic_fluid_index_cm', 'label': 'ИАЖ (см)', 'type': 'number', 'step': '0.1'}, { 'name': 'amniotic_fluid_max_pocket_cm', 'label': 'Макс. верт. карман (см)', 'type': 'number', 'step': '0.1'}, { 'name': 'amniotic_fluid_volume_assessment', 'label': 'Оценка количества', 'type': 'select', 'options': ['Нормальное', 'Маловодие', 'Многоводие']}, { 'name': 'amniotic_fluid_features', 'label': 'Особенности вод', 'type': 'textarea', 'placeholder': 'Напр: Прозрачные, с мелкодисп. взвесью...'}, { 'name': 'umbilical_cord_header', 'label': 'Пуповина:', 'type': 'header' }, { 'name': 'umbilical_cord_vessels', 'label': 'Количество сосудов', 'type': 'select', 'options': ['3 (2А+1В)', '2 (1А+1В)', 'Другое']}, { 'name': 'umbilical_cord_features', 'label': 'Особенности пуповины', 'type': 'textarea', 'placeholder': 'Напр: Обвитие вокруг шеи, предлежание...' }, { 'name': 'cervix_header', 'label': 'Шейка матки:', 'type': 'header' }, { 'name': 'cervix_length_mm', 'label': 'Длина (мм)', 'type': 'number', 'step': '1'}, { 'name': 'cervical_canal_state', 'label': 'Цервикальный канал', 'type': 'select', 'options': ['Закрыт на всем протяжении', 'Расширен (описание)']}, { 'name': 'internal_os_state', 'label': 'Внутренний зев', 'type': 'select', 'options': ['Закрыт', 'Открыт (V/U- форма)']}, { 'name': 'uterus_ovaries_header', 'label': 'Матка и яичники:', 'type': 'header' }, { 'name': 'uterus_features', 'label': 'Особенности матки', 'type': 'textarea', 'placeholder': 'Напр: Миоматозные узлы (локализация, размеры), тонус'}, { 'name': 'ovaries_features', 'label': 'Особенности яичников', 'type': 'textarea', 'placeholder': 'Напр: Кисты, образования'}, { 'name': 'doppler_header', 'label': 'Допплерометрия (если проводилась):', 'type': 'header' }, { 'name': 'doppler_uterine_artery_r_pi', 'label': 'ПМА ПИ (правая)', 'type': 'number', 'step': '0.01'}, { 'name': 'doppler_uterine_artery_l_pi', 'label': 'ПМА ПИ (левая)', 'type': 'number', 'step': '0.01'}, { 'name': 'doppler_umbilical_artery_pi', 'label': 'АП ПИ', 'type': 'number', 'step': '0.01'}, { 'name': 'doppler_middle_cerebral_artery_pi', 'label': 'СМА ПИ', 'type': 'number', 'step': '0.01'}, { 'name': 'doppler_cpr', 'label': 'ЦПО (СМА ПИ / АП ПИ)', 'type': 'number', 'step': '0.01'}, { 'name': 'conclusion_header', 'label': 'Заключение:', 'type': 'header' }, { 'name': 'conclusion_pregnancy', 'label': 'Беременность (соответствует нед/дням)', 'type': 'text', 'placeholder': 'Напр: 20 недель 1 день' }, { 'name': 'conclusion_features', 'label': 'Особенности', 'type': 'textarea', 'placeholder': 'Краткое описание основных находок, ВПР (если есть), маркеров ХА...' }, { 'name': 'recommendations', 'label': 'Рекомендации', 'type': 'textarea', 'placeholder': 'Напр: УЗИ контроль в динамике, консультация...' } ] }, 'УЗИ при замершей беременности': { 'category': 'Замершая беременность', 'fields': [ { 'name': 'amenorrhea_weeks', 'label': 'Срок аменореи (недель)', 'type': 'number' }, { 'name': 'last_menstrual_period_date', 'label': 'Дата последней менструации', 'type': 'date' }, { 'name': 'uterus_size_weeks', 'label': 'Размер матки соответствует (недель)', 'type': 'number' }, { 'name': 'fetal_egg_location', 'label': 'Локализация плодного яйца', 'type': 'select', 'options': ['В полости матки', 'Внематочная'] }, { 'name': 'fetal_egg_shape', 'label': 'Форма плодного яйца', 'type': 'select', 'options': ['Правильная', 'Деформированное'] }, { 'name': 'fetal_egg_svd_mm', 'label': 'СВД плодного яйца (мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'embryo_visualized', 'label': 'Эмбрион визуализируется', 'type': 'select', 'options': ['Да', 'Нет'] }, { 'name': 'embryo_ktr_mm', 'label': 'КТР эмбриона (мм)', 'type': 'number', 'step': '0.1', 'condition': { 'field': 'embryo_visualized', 'value': 'Да' } }, { 'name': 'heartbeat_visualized', 'label': 'Сердцебиение эмбриона', 'type': 'select', 'options': ['+', '-', 'Не определяется'], 'condition': { 'field': 'embryo_visualized', 'value': 'Да' } }, { 'name': 'yolk_sac_visualized', 'label': 'Желточный мешок', 'type': 'select', 'options': ['Да', 'Нет'] }, { 'name': 'yolk_sac_diameter_mm', 'label': 'Диаметр желточного мешка (мм)', 'type': 'number', 'step': '0.1', 'condition': { 'field': 'yolk_sac_visualized', 'value': 'Да' } }, { 'name': 'chorion_status', 'label': 'Состояние хориона', 'type': 'select', 'options': ['Не изменено', 'Изменения (описать)'] }, { 'name': 'chorion_changes_desc', 'label': 'Описание изменений хориона', 'type': 'textarea', 'condition': { 'field': 'chorion_status', 'value': 'Изменения (описать)' } }, { 'name': 'amniotic_fluid_status', 'label': 'Околоплодные воды', 'type': 'select', 'options': ['Не изменены', 'Изменения (описать)'] }, { 'name': 'amniotic_fluid_changes_desc', 'label': 'Описание изменений вод', 'type': 'textarea', 'condition': { 'field': 'amniotic_fluid_status', 'value': 'Изменения (описать)' } }, { 'name': 'uterus_tone', 'label': 'Тонус матки', 'type': 'select', 'options': ['Нормотонус', 'Гипертонус'] }, { 'name': 'ovaries_features', 'label': 'Особенности яичников', 'type': 'textarea', 'placeholder': 'Напр: Киста желтого тела' }, { 'name': 'conclusion', 'label': 'Заключение', 'type': 'textarea', 'placeholder': 'Например: Неразвивающаяся беременность.' } ] }, 'УЗИ миомы матки': { 'category': 'Миома матки', 'fields': [ { 'name': 'uterus_size_weeks', 'label': 'Размер матки соответствует неделям беременности', 'type': 'text', 'placeholder': 'Например: 12-13 недель' }, { 'name': 'uterus_position', 'label': 'Положение матки', 'type': 'select', 'options': ['Anteflexio', 'Retroflexio', 'Другое'] }, { 'name': 'uterus_contours', 'label': 'Контуры матки', 'type': 'select', 'options': ['Ровные', 'Неровные, бугристые'] }, { 'name': 'myometrium_structure', 'label': 'Структура миометрия', 'type': 'select', 'options': ['Однородная', 'Неоднородная'] }, { 'name': 'myomatous_nodes_count', 'label': 'Количество миоматозных узлов', 'type': 'number' }, { 'name': 'myomatous_nodes_location_size', 'label': 'Локализация и размеры узлов', 'type': 'textarea', 'placeholder': 'Описание локализации, размеров, структуры узлов' }, { 'name': 'endometrium_thickness_mm', 'label': 'Толщина эндометрия (М-эхо, мм)', 'type': 'number', 'step': '0.1' }, { 'name': 'endometrium_structure', 'label': 'Структура эндометрия', 'type': 'select', 'options': ['Не изменена', 'Изменена (описать)'] }, { 'name': 'endometrium_changes_desc', 'label': 'Описание изменений эндометрия', 'type': 'textarea', 'condition': { 'field': 'endometrium_structure', 'value': 'Изменена (описать)' } }, { 'name': 'cervix_features', 'label': 'Особенности шейки матки', 'type': 'textarea', 'placeholder': 'Кисты, деформации и т.д.' }, { 'name': 'ovaries_visualized', 'label': 'Яичники визуализируются', 'type': 'select', 'options': ['Да', 'Нет', 'Затруднена визуализация'] }, { 'name': 'ovaries_features', 'label': 'Особенности яичников', 'type': 'textarea', 'placeholder': 'Описание' }, { 'name': 'adnexa_features', 'label': 'Особенности придатков матки', 'type': 'textarea', 'placeholder': 'Описание' }, { 'name': 'free_fluid_pelvis', 'label': 'Свободная жидкость в малом тазу', 'type': 'select', 'options': ['Нет', 'Есть, в незначительном количестве', 'Есть, в умеренном количестве', 'Есть, в значительном количестве'] }, { 'name': 'conclusion', 'label': 'Заключение', 'type': 'textarea', 'placeholder': 'Например: Миома матки.' }, { 'name': 'recommendations', 'label': 'Рекомендации', 'type': 'textarea', 'placeholder': 'Например: Контроль УЗИ через 3-6 месяцев.' } ] }, } def download_db_from_hf(file_path): if not HF_TOKEN_READ or not REPO_ID: logging.warning(f"Пропуск скачивания {file_path}: HF_TOKEN_READ или REPO_ID не установлен.") return False try: logging.info(f"Попытка скачивания {file_path} из {REPO_ID}...") hf_hub_download( repo_id=REPO_ID, filename=file_path, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False ) logging.info(f"{file_path} успешно скачан из Hugging Face.") time.sleep(0.5) return True except RepositoryNotFoundError: logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face.") return False except HFValidationError as e: logging.warning(f"Ошибка валидации при скачивании {file_path} из Hugging Face: {e}. Возможно, файл отсутствует в репозитории {REPO_ID}.") return False except Exception as e: if "404" in str(e) or "not found" in str(e).lower() or "EntryNotFoundError" in str(e): logging.warning(f"Файл {file_path} не найден в репозитории {REPO_ID}. Если он существует локально, он будет использоваться. Если нет - будет создан новый.") return False logging.error(f"Неизвестная ошибка при скачивании {file_path} из Hugging Face: {type(e).__name__} - {e}") return False def load_data(file_path): with db_lock: download_successful = download_db_from_hf(file_path) try: if not os.path.exists(file_path): logging.warning(f"Локальный файл {file_path} не найден (скачивание не удалось или файла нет в репо). Создание нового пустого файла.") with open(file_path, 'w', encoding='utf-8') as f: json.dump([], f) return [] with open(file_path, 'r', encoding='utf-8') as file: content = file.read() if not content: logging.warning(f"Файл {file_path} пуст. Возвращается пустой список.") return [] data = json.loads(content) return data except json.JSONDecodeError: logging.error(f"Ошибка: Невозможно декодировать JSON файл {file_path}. Файл может быть поврежден. Создается резервная копия и новый пустой файл.") try: corrupted_backup_path = f"{file_path}.corrupted_{datetime.now().strftime('%Y%m%d%H%M%S')}" os.rename(file_path, corrupted_backup_path) logging.info(f"Поврежденный файл {file_path} переименован в {corrupted_backup_path}") except Exception as backup_e: logging.error(f"Не удалось создать резервную копию поврежденного файла {file_path}: {backup_e}") with open(file_path, 'w', encoding='utf-8') as f: json.dump([], f) return [] except Exception as e: logging.error(f"Произошла ошибка при загрузке данных из {file_path}: {e}") return [] def save_data(file_path, data): with db_lock: try: temp_file_path = file_path + ".tmp" with open(temp_file_path, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) os.replace(temp_file_path, file_path) logging.info(f"Данные успешно сохранены в {file_path}") upload_db_to_hf(file_path) except Exception as e: logging.error(f"Ошибка при сохранении данных в {file_path}: {e}") if os.path.exists(temp_file_path): try: os.remove(temp_file_path) except Exception as remove_e: logging.error(f"Не удалось удалить временный файл {temp_file_path}: {remove_e}") raise def upload_db_to_hf(file_path): if not HF_TOKEN_WRITE or not REPO_ID: return if not os.path.exists(file_path): logging.warning(f"Пропуск загрузки: Файл {file_path} не найден локально.") return try: api = HfApi() api.upload_file( path_or_fileobj=file_path, path_in_repo=os.path.basename(file_path), repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Авто-бэкап: {os.path.basename(file_path)} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) logging.info(f"Резервная копия {file_path} успешно загружена на Hugging Face.") except Exception as e: logging.error(f"Ошибка при загрузке {file_path} на Hugging Face: {type(e).__name__} - {e}") def periodic_backup(): logging.info("Запуск периодического резервного копирования...") while True: logging.info("Начало цикла резервного копирования.") for db_file in [PATIENTS_DB, PROTOCOLS_DB, CONTROL_DB]: logging.debug(f"Попытка загрузки {db_file} на HF...") upload_db_to_hf(db_file) time.sleep(1) logging.info(f"Цикл резервного копирования завершен. Ожидание {800} секунд...") time.sleep(800) def initialize_data(): logging.info("Инициализация данных при запуске...") for db_file in [PATIENTS_DB, PROTOCOLS_DB, CONTROL_DB]: load_data(db_file) logging.info("Инициализация данных завершена.") @app.route('/') def index(): patients = load_data(PATIENTS_DB) protocols = load_data(PROTOCOLS_DB) control = load_data(CONTROL_DB) patient_dict = {p['id']: p['name'] for p in patients} return ''' Медицинский центр

Медицинский центр "Ырыс-Медиа"

Регистрация пациента

Список пациентов

''' + ''.join([f''' ''' for p in sorted(patients, key=lambda x: x.get('id', 0))]) + '''
IDФИОТелефонГод рожд.Действия
{p.get("id", "N/A")} {p.get("name", "N/A")} {p.get("phone", "N/A")} {p.get("dob", "N/A")}

Новый протокол

Выберите тип протокола:


Список созданных протоколов

''' + ''.join([f''' ''' for p in sorted(protocols, key=lambda x: x.get('id', 0), reverse=True)]) + '''
IDПациентТип протоколаДатаДействия
{p.get("id", "N/A")} {patient_dict.get(p.get("patient_id"), "Пациент не найден")} (ID: {p.get("patient_id")}) {p.get("type", "N/A")} {p.get("date", "N/A")}

Добавить пациента на контроль

Пациенты на контроле

''' + ''.join([f''' ''' for c in sorted(control, key=lambda x: x.get('date', ''), reverse=True)]) + '''
ФИОПричинаДата постановкиДействия
{c.get("name", "N/A")} {c.get("reason", "N/A")} {c.get("date", "N/A")}

База данных пациентов и исследований

ФИОТелефонГод рожд.Протоколы исследований
Загрузка...

Администрирование

Резервное копирование и восстановление (Hugging Face)

Данные сохраняются автоматически при каждом изменении и периодически (если настроен токен записи).

Кнопки ниже для ручного управления:

''' @app.route('/add_patient', methods=['POST']) def add_patient(): patients = load_data(PATIENTS_DB) control = load_data(CONTROL_DB) try: name = request.form.get('name', '').strip() phone = request.form.get('phone', '').strip() dob = request.form.get('dob') control_checkbox = request.form.get('control_checkbox') control_reason = request.form.get('control_reason', '').strip() if not name or not phone: return jsonify({'status': 'error', 'error': 'ФИО и телефон обязательны.'}), 400 if any(p.get('name') == name and p.get('phone') == phone for p in patients): return jsonify({'status': 'error', 'error': 'Пациент с таким ФИО и телефоном уже существует.'}), 409 new_id = (max(p.get('id', 0) for p in patients) if patients else 0) + 1 new_patient = { 'id': new_id, 'name': name, 'phone': phone, 'dob': dob if dob else None } patients.append(new_patient) save_data(PATIENTS_DB, patients) logging.info(f"Добавлен пациент: ID={new_id}, Имя={name}") control_entry = None if control_checkbox and control_reason: existing_control = next((c for c in control if c.get('patient_id') == new_id), None) if not existing_control: new_control_entry = { 'patient_id': new_id, 'name': name, 'reason': control_reason, 'date': datetime.now().strftime('%Y-%m-%d') } control.append(new_control_entry) save_data(CONTROL_DB, control) control_entry = new_control_entry logging.info(f"Пациент ID={new_id} также добавлен на контроль. Причина: {control_reason}") else: logging.warning(f"Пациент ID={new_id} уже был на контроле. Не добавлен повторно при создании пациента.") return jsonify({'status': 'success', 'patient': new_patient, 'control_entry': control_entry}), 201 except Exception as e: logging.exception(f"Ошибка при добавлении пациента") return jsonify({'status': 'error', 'error': 'Внутренняя ошибка сервера при добавлении пациента.'}), 500 @app.route('/delete_patient/', methods=['POST']) def delete_patient(patient_id): try: patients = load_data(PATIENTS_DB) protocols = load_data(PROTOCOLS_DB) control = load_data(CONTROL_DB) initial_patient_count = len(patients) initial_protocol_count = len(protocols) initial_control_count = len(control) patients_new = [p for p in patients if p.get('id') != patient_id] protocols_new = [p for p in protocols if p.get('patient_id') != patient_id] control_new = [c for c in control if c.get('patient_id') != patient_id] if len(patients_new) == initial_patient_count: return jsonify({'status': 'error', 'error': 'Пациент с таким ID не найден.'}), 404 save_data(PATIENTS_DB, patients_new) save_data(PROTOCOLS_DB, protocols_new) save_data(CONTROL_DB, control_new) logging.info(f"Удален пациент ID={patient_id} и связанные данные (протоколов: {initial_protocol_count - len(protocols_new)}, контроль: {initial_control_count - len(control_new)})") return jsonify({'status': 'success'}), 200 except Exception as e: logging.exception(f"Ошибка при удалении пациента ID={patient_id}") return jsonify({'status': 'error', 'error': 'Внутренняя ошибка сервера при удалении пациента.'}), 500 @app.route('/add_protocol', methods=['POST']) def add_protocol(): protocols = load_data(PROTOCOLS_DB) patients = load_data(PATIENTS_DB) try: patient_id_str = request.form.get('patient_id') protocol_type = request.form.get('protocol_type') protocol_date_str = request.form.get('protocol_date') if not patient_id_str or not protocol_type or not protocol_date_str: return jsonify({'status': 'error', 'error': 'Не указан пациент, тип протокола или дата.'}), 400 try: patient_id = int(patient_id_str) except ValueError: return jsonify({'status': 'error', 'error': 'Неверный формат ID пациента.'}), 400 patient = next((p for p in patients if p.get('id') == patient_id), None) if not patient: return jsonify({'status': 'error', 'error': f'Пациент с ID {patient_id} не найден.'}), 404 try: protocol_date = datetime.strptime(protocol_date_str, '%Y-%m-%d').strftime('%Y-%m-%d') except ValueError: return jsonify({'status': 'error', 'error': 'Неверный формат даты исследования.'}), 400 protocol_data = {} exclude_keys = ['patient_id', 'protocol_type', 'protocol_date'] for key, value in request.form.items(): if key not in exclude_keys: cleaned_value = value.strip() if isinstance(value, str) else value if cleaned_value not in [None, ""]: protocol_data[key] = cleaned_value if not protocol_data: return jsonify({'status': 'error', 'error': 'Нет данных для сохранения в протоколе.'}), 400 new_id = (max(p.get('id', 0) for p in protocols) if protocols else 0) + 1 new_protocol = { 'id': new_id, 'patient_id': patient_id, 'type': protocol_type, 'date': protocol_date, 'creation_timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'data': protocol_data } protocols.append(new_protocol) save_data(PROTOCOLS_DB, protocols) logging.info(f"Добавлен протокол: ID={new_id}, Тип={protocol_type}, ПациентID={patient_id}, Дата={protocol_date}") new_protocol['patient_name'] = patient.get('name', 'Не найден') return jsonify({'status': 'success', 'protocol': new_protocol}), 201 except Exception as e: logging.exception(f"Ошибка при добавлении протокола") return jsonify({'status': 'error', 'error': 'Внутренняя ошибка сервера при добавлении протокола.'}), 500 @app.route('/get_protocol/') def get_protocol(protocol_id): try: protocols = load_data(PROTOCOLS_DB) patients = load_data(PATIENTS_DB) protocol = next((p for p in protocols if p.get('id') == protocol_id), None) if protocol: patient = next((pt for pt in patients if pt.get('id') == protocol.get('patient_id')), None) protocol['patient_name'] = patient.get('name', 'Не найден') if patient else 'Не найден' protocol['patient_dob'] = patient.get('dob', 'N/A') if patient else 'N/A' return jsonify(protocol) else: return jsonify({'error': 'Протокол не найден'}), 404 except Exception as e: logging.exception(f"Ошибка при получении протокола ID={protocol_id}") return jsonify({'error': 'Внутренняя ошибка сервера'}), 500 @app.route('/get_patient_name/') def get_patient_name(patient_id): try: patients = load_data(PATIENTS_DB) patient = next((pt for pt in patients if pt.get('id') == patient_id), None) if patient: return jsonify({'name': patient.get('name', 'Не найден')}) else: return jsonify({'name': 'Пациент не найден'}), 404 except Exception as e: logging.exception(f"Ошибка при получении имени пациента ID={patient_id}") return jsonify({'error': 'Внутренняя ошибка сервера'}), 500 @app.route('/edit_protocol/', methods=['POST']) def edit_protocol(protocol_id): protocols = load_data(PROTOCOLS_DB) try: protocol_to_edit = next((p for p in protocols if p.get('id') == protocol_id), None) if not protocol_to_edit: return jsonify({'status': 'error', 'error': 'Протокол для редактирования не найден.'}), 404 protocol_data = {} exclude_keys = ['patient_id', 'protocol_type', 'protocol_date', 'id', 'creation_timestamp', 'last_updated_timestamp'] for key, value in request.form.items(): if key not in exclude_keys: cleaned_value = value.strip() if isinstance(value, str) else value if cleaned_value not in [None, ""]: protocol_data[key] = cleaned_value if not protocol_data: return jsonify({'status': 'error', 'error': 'Нет данных для обновления протокола.'}), 400 protocol_to_edit['data'] = protocol_data protocol_to_edit['last_updated_timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') save_data(PROTOCOLS_DB, protocols) logging.info(f"Обновлен протокол: ID={protocol_id}") return jsonify({'status': 'success', 'protocol': protocol_to_edit}), 200 except Exception as e: logging.exception(f"Ошибка при редактировании протокола ID={protocol_id}") return jsonify({'status': 'error', 'error': 'Внутренняя ошибка сервера при редактировании протокола.'}), 500 @app.route('/delete_protocol/', methods=['POST']) def delete_protocol(protocol_id): protocols = load_data(PROTOCOLS_DB) try: initial_count = len(protocols) protocols_new = [p for p in protocols if p.get('id') != protocol_id] if len(protocols_new) == initial_count: return jsonify({'status': 'error', 'error': 'Протокол с таким ID не найден.'}), 404 save_data(PROTOCOLS_DB, protocols_new) logging.info(f"Удален протокол: ID={protocol_id}") return jsonify({'status': 'success'}), 200 except Exception as e: logging.exception(f"Ошибка при удалении протокола ID={protocol_id}") return jsonify({'status': 'error', 'error': 'Внутренняя ошибка сервера при удалении протокола.'}), 500 @app.route('/add_control', methods=['POST']) def add_control(): control = load_data(CONTROL_DB) patients = load_data(PATIENTS_DB) try: patient_id_str = request.form.get('patient_id') reason = request.form.get('reason', '').strip() if not patient_id_str or not reason: return jsonify({'status': 'error', 'error': 'Не выбран пациент или не указана причина.'}), 400 try: patient_id = int(patient_id_str) except ValueError: return jsonify({'status': 'error', 'error': 'Неверный формат ID пациента.'}), 400 patient = next((p for p in patients if p.get('id') == patient_id), None) if not patient: return jsonify({'status': 'error', 'error': f'Пациент с ID {patient_id} не найден.'}), 404 if any(c.get('patient_id') == patient_id for c in control): return jsonify({'status': 'error', 'error': 'Этот пациент уже стоит на контроле.'}), 409 new_control_entry = { 'patient_id': patient_id, 'name': patient.get('name', 'Имя не найдено'), 'reason': reason, 'date': datetime.now().strftime('%Y-%m-%d') } control.append(new_control_entry) save_data(CONTROL_DB, control) logging.info(f"Пациент ID={patient_id} добавлен на контроль. Причина: {reason}") return jsonify({'status': 'success', 'control_entry': new_control_entry}), 201 except Exception as e: logging.exception(f"Ошибка при добавлении на контроль") return jsonify({'status': 'error', 'error': 'Внутренняя ошибка сервера при добавлении на контроль.'}), 500 @app.route('/remove_control/', methods=['POST']) def remove_control(patient_id): control = load_data(CONTROL_DB) try: initial_count = len(control) control_new = [c for c in control if c.get('patient_id') != patient_id] if len(control_new) == initial_count: return jsonify({'status': 'error', 'error': 'Пациент с таким ID не найден в списке контроля.'}), 404 save_data(CONTROL_DB, control_new) logging.info(f"Пациент ID={patient_id} снят с контроля.") return jsonify({'status': 'success'}), 200 except Exception as e: logging.exception(f"Ошибка при снятии с контроля пациента ID={patient_id}") return jsonify({'status': 'error', 'error': 'Внутренняя ошибка сервера при снятии с контроля.'}), 500 @app.route('/search') def search(): try: query = request.args.get('query', '').lower().strip() patients = load_data(PATIENTS_DB) protocols = load_data(PROTOCOLS_DB) result = [] protocols_by_patient = {} for p in protocols: pid = p.get("patient_id") if pid: if pid not in protocols_by_patient: protocols_by_patient[pid] = [] protocols_by_patient[pid].append(f'{p.get("date", "N/A")}: {p.get("type", "N/A")} (ID: {p.get("id")})') for pid in protocols_by_patient: protocols_by_patient[pid].sort(key=lambda x: x.split(':')[0], reverse=True) for patient in patients: match = query in patient.get('name', '').lower() if query else True if match: patient_protocols = protocols_by_patient.get(patient.get("id"), []) result.append({ 'id': patient.get("id"), 'name': patient.get("name"), 'phone': patient.get("phone"), 'dob': patient.get("dob"), 'protocols': patient_protocols }) result_sorted = sorted(result, key=lambda x: x.get('name', '')) return jsonify(result_sorted) except Exception as e: logging.exception(f"Ошибка при поиске ('{query}')") return jsonify({'error': 'Внутренняя ошибка сервера при поиске.'}), 500 @app.route('/backup', methods=['POST']) def backup(): logging.info("Запрос ручного резервного копирования...") if not HF_TOKEN_WRITE: return "Ошибка: Токен для записи (HF_TOKEN) не настроен.", 403 backup_errors = [] for db_file in [PATIENTS_DB, PROTOCOLS_DB, CONTROL_DB]: try: upload_db_to_hf(db_file) except Exception as e: logging.error(f"Ошибка ручного бэкапа файла {db_file}: {e}") backup_errors.append(db_file) if not backup_errors: logging.info("Ручное резервное копирование завершено успешно.") return "Резервная копия успешно создана.", 200 else: logging.warning(f"Ручное резервное копирование завершено с ошибками для файлов: {', '.join(backup_errors)}") return f"Резервная копия создана с ошибками для файлов: {', '.join(backup_errors)}", 500 @app.route('/download', methods=['GET']) def download(): logging.info("Запрос ручного скачивания баз данных...") if not HF_TOKEN_READ: return "Ошибка: Токен для чтения (HF_TOKEN_READ) не настроен.", 403 download_errors = [] download_success = [] for db_file in [PATIENTS_DB, PROTOCOLS_DB, CONTROL_DB]: if download_db_from_hf(db_file): download_success.append(db_file) load_data(db_file) else: download_errors.append(db_file) if not download_errors: logging.info("Ручное скачивание завершено успешно.") return "База данных успешно скачана со сервера.", 200 elif download_success: logging.warning(f"Скачивание завершено. Ошибки для файлов: {', '.join(download_errors)}") return f"Скачивание завершено. Ошибки для файлов: {', '.join(download_errors)}", 207 else: logging.error("Не удалось скачать ни один файл базы данных.") return "Не удалось скачать файлы базы данных с сервера.", 500 @app.route('/print_protocol/') def print_protocol_html(protocol_id): protocol_response = get_protocol(protocol_id) if protocol_response.status_code != 200: logging.error(f"Print HTML: Protocol {protocol_id} not found or error fetching.") return "Протокол не найден или ошибка получения данных", 404 protocol_data_full = protocol_response.json if not protocol_data_full or 'data' not in protocol_data_full: logging.error(f"Print HTML: Invalid data format for protocol {protocol_id}.") return "Неверный формат данных протокола", 500 protocol_data = protocol_data_full['data'] protocol_type = protocol_data_full.get('type', 'Неизвестный тип') patient_name = protocol_data_full.get('patient_name', 'Имя не указано') patient_dob = protocol_data_full.get('patient_dob', 'N/A') protocol_date = protocol_data_full.get('date', 'Дата не указана') protocol_id_num = protocol_data_full.get('id', 'N/A') logging.info(f"Generating printable HTML for protocol ID: {protocol_id}") definition = protocolDefinitions.get(protocol_type, {'fields': []}) processed_keys = set(['patient_id', 'protocol_type', 'protocol_date', 'id', 'creation_timestamp', 'last_updated_timestamp']) content_html = "" if definition and definition.get('fields'): for field_def in definition['fields']: field_name = field_def['name'] field_label = field_def['label'] processed_keys.add(field_name) if field_def['type'] == 'header': content_html += f"

{field_label}

" elif field_def['type'] != 'note' and field_name in protocol_data: value = protocol_data[field_name] if value is not None and str(value).strip() != '': value_display = str(value).replace('\n', '
') content_html += f"

{field_label}: {value_display}

" other_data_header_added = False for key, value in protocol_data.items(): if key not in processed_keys and not key.endswith('_label') and not key.endswith('_header'): if value is not None and str(value).strip() != '': if not other_data_header_added: content_html += f"

Прочие данные:

" other_data_header_added = True label = key.replace('_', ' ').capitalize() value_display = str(value).replace('\n', '
') content_html += f"

{label}: {value_display}

" else: content_html += f"

Данные исследования:

" for key, value in protocol_data.items(): if value is not None and str(value).strip() != '' and not key.endswith('_label') and not key.endswith('_header'): label = key.replace('_', ' ').capitalize() value_display = str(value).replace('\n', '
') content_html += f"

{label}: {value_display}

" html_template = f""" Протокол №{protocol_id_num} - {protocol_type}
«Ырыс-Медиа» Медициналык Борбору
Бишкек ш. Кулиев/Киев «Кенч» соода борбору 3-кабат
0778 70 90 52, 0500 70 90 52
Инстаграм: yrysmedcentre
«Ырыс-Медиа» Медицинский центр
г. Бишкек, ул. Кулиева/Киев, торговый центр «Кенч», 3-этаж
0778 70 90 52, 0500 70 90 52
Инстаграм: yrysmedcentre
{protocol_type} - Протокол №{protocol_id_num}

Пациент: {patient_name}

Год рождения: {patient_dob}

Дата исследования: {protocol_date}

{content_html}
""" return render_template_string(html_template) if __name__ == '__main__': initialize_data() if HF_TOKEN_WRITE and REPO_ID: backup_thread = threading.Thread(target=periodic_backup, daemon=True) backup_thread.start() else: logging.warning("Автоматическое резервное копирование отключено.") logging.info(f"Запуск Flask приложения на порту 7860...") try: from waitress import serve serve(app, host='0.0.0.0', port=7860) except ImportError: logging.warning("Waitress не найден. Запуск через встроенный сервер Flask (НЕ рекомендуется для production).") app.run(host='0.0.0.0', port=7860, debug=False)