Medcentr / app.py
Shveiauto's picture
Rename Soola.txt to app.py
bf636ea verified
# -*- 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 '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Медицинский центр</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
padding: 10px;
color: #333;
font-size: 16px;
}
.container {
max-width: 1200px;
margin: 15px auto;
background: rgba(255, 255, 255, 0.98);
border-radius: 15px;
box-shadow: 0 10px 35px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(90deg, #56ab2f, #a8e063);
color: white;
padding: 20px 25px;
text-align: center;
}
.header h1 {
font-size: 2.2em;
font-weight: 600;
text-shadow: 1px 1px 3px rgba(0,0,0,0.2);
}
.tabs {
display: flex;
flex-wrap: wrap;
background: #4a5568;
padding: 8px;
gap: 8px;
}
.tab-btn {
flex: 1;
min-width: 90px;
padding: 12px 10px;
background: rgba(255,255,255,0.1);
border: none;
color: white;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 8px;
font-weight: 500;
font-size: 0.95em;
text-align: center;
}
.tab-btn:hover { background: #68d391; }
.tab-btn.active {
background: #56ab2f;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.2);
}
.tab {
display: none;
padding: 25px;
animation: fadeIn 0.4s ease-in-out;
}
.tab.active { display: block; }
h2 {
color: #2c5282;
margin-bottom: 20px;
font-weight: 600;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 10px;
}
.form-section { margin-bottom: 25px; }
.form-group {
margin-bottom: 18px;
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-weight: 500;
color: #4a5568;
font-size: 0.9em;
}
input[type="text"], input[type="number"], input[type="date"], select, textarea {
padding: 12px 15px;
border: 1px solid #cbd5e0;
border-radius: 8px;
font-size: 1em;
background: #f7fafc;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
transition: all 0.2s ease;
width: 100%;
}
input:focus, select:focus, textarea:focus {
border-color: #68d391;
box-shadow: 0 0 0 3px rgba(104, 211, 145, 0.3);
outline: none;
background: white;
}
textarea { min-height: 100px; resize: vertical; }
button[type="submit"], .action-btn, .protocol-select-btn {
padding: 12px 25px;
background: linear-gradient(90deg, #56ab2f, #a8e063);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
font-size: 1em;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
margin-top: 5px;
}
button[type="submit"]:hover, .action-btn:hover, .protocol-select-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(0,0,0,0.15);
filter: brightness(1.1);
}
.protocol-selector button { margin: 5px; }
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
font-size: 0.95em;
}
th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #e2e8f0;
}
th {
background: #e2e8f0;
color: #4a5568;
font-weight: 600;
text-transform: uppercase;
font-size: 0.85em;
}
tr { transition: background-color 0.2s ease; }
tr:nth-child(even) { background: #f7fafc; }
tr:hover { background: #edf2f7; }
.action-btn {
padding: 6px 12px;
margin: 0 3px;
font-size: 0.85em;
border-radius: 6px;
}
.edit-btn { background: linear-gradient(90deg, #3498db, #2980b9); }
.delete-btn { background: linear-gradient(90deg, #e74c3c, #c0392b); }
.view-btn { background: linear-gradient(90deg, #f39c12, #e67e22); }
.remove-btn { background: linear-gradient(90deg, #95a5a6, #7f8c8d); }
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 25px 30px;
border-radius: 12px;
box-shadow: 0 15px 40px rgba(0,0,0,0.2);
z-index: 1000;
max-width: 90%;
width: 600px;
max-height: 85vh;
overflow-y: auto;
}
.modal h3 {
margin-bottom: 15px;
color: #2c5282;
font-weight: 600;
padding-bottom: 10px;
border-bottom: 1px solid #e2e8f0;
}
.modal .form-group { margin-bottom: 15px; }
.modal button.close-modal-btn { margin-right: 10px; margin-top: 15px; background: linear-gradient(90deg, #95a5a6, #7f8c8d); }
.modal button:not(.close-modal-btn) { margin-right: 10px; margin-top: 10px;}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.6);
z-index: 999;
cursor: pointer;
}
@media (max-width: 768px) {
body { font-size: 14px; padding: 5px;}
.container { margin: 5px auto; border-radius: 10px; }
.header h1 { font-size: 1.8em; }
.tabs { flex-direction: column; padding: 5px; }
.tab-btn { padding: 10px; margin: 2px 0; }
.tab { padding: 15px; }
th, td { padding: 8px 10px; }
.action-btn { padding: 5px 8px; margin: 2px; }
.modal { width: 95%; padding: 20px; }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.protocol-selector {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #e2e8f0;
}
.protocol-selector button {
background: linear-gradient(90deg, #4facfe, #00f2fe);
flex-basis: calc(33.33% - 10px);
}
#protocol-form-container { margin-top: 20px; }
.protocol-field { margin-bottom: 15px; }
.protocol-field label { display: block; margin-bottom: 5px; font-weight: 500; color: #4a5568;}
.protocol-field input, .protocol-field select, .protocol-field textarea { width: 100%; }
.protocol-field h4 {
margin-top: 15px;
margin-bottom: 10px;
color: #2c5282;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 5px;
font-size: 1.1em;
}
.protocol-anatomy-table {
display: grid;
grid-template-columns: 1fr auto auto auto;
gap: 5px 10px;
align-items: center;
margin-bottom: 10px;
}
.protocol-anatomy-table label { font-weight: normal; margin-bottom: 0;}
.protocol-anatomy-table select { padding: 5px; font-size: 0.9em; }
.protocol-view-data { font-size: 0.95em; }
.protocol-view-data p { margin-bottom: 8px; line-height: 1.5; }
.protocol-view-data strong { color: #2d3748; margin-right: 5px; display: inline-block; min-width: 150px; }
.protocol-view-data hr { border: 0; height: 1px; background: #e2e8f0; margin: 15px 0; }
.protocol-view-data h4 {
margin-top: 15px;
margin-bottom: 10px;
color: #2c5282;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 5px;
font-size: 1.1em;
font-weight: 600;
}
.protocol-categories {
display: flex;
flex-direction: column;
gap: 15px;
}
.protocol-category {
border-bottom: 1px solid #e2e8f0;
padding-bottom: 15px;
margin-bottom: 15px;
}
.protocol-category h3 {
color: #374151;
margin-bottom: 10px;
}
.protocol-category-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Медицинский центр "Ырыс-Медиа"</h1>
</div>
<div class="tabs">
<button class="tab-btn active" onclick="showTab('reception')">Прием</button>
<button class="tab-btn" onclick="showTab('protocols')">Протоколы</button>
<button class="tab-btn" onclick="showTab('control')">Контроль</button>
<button class="tab-btn" onclick="showTab('database')">База</button>
<button class="tab-btn" onclick="showTab('admin')">Админ</button>
</div>
<div id="reception" class="tab active">
<h2>Регистрация пациента</h2>
<div class="form-section">
<form id="addPatientForm">
<div class="form-group">
<label for="name">ФИО Пациента:</label>
<input type="text" id="name" name="name" placeholder="Введите полное имя" required>
</div>
<div class="form-group">
<label for="phone">Номер телефона:</label>
<input type="text" id="phone" name="phone" placeholder="Например, 0770123456" required>
</div>
<div class="form-group">
<label for="dob">Год рождения:</label>
<input type="number" id="dob" name="dob" placeholder="Например, 1990" min="1900" max="''' + str(datetime.now().year) + '''">
</div>
<div class="form-group">
<label for="control_checkbox">Поставить на контроль:</label>
<input type="checkbox" id="control_checkbox" name="control_checkbox">
</div>
<div class="form-group" id="control_reason_group" style="display: none;">
<label for="control_reason">Причина контроля:</label>
<textarea name="control_reason" id="control_reason" placeholder="Укажите причину постановки на контроль"></textarea>
</div>
<button type="submit">Добавить пациента</button>
</form>
</div>
<h2>Список пациентов</h2>
<table id="patientsTable">
<thead>
<tr><th>ID</th><th>ФИО</th><th>Телефон</th><th>Год рожд.</th><th>Действия</th></tr>
</thead>
<tbody>
''' + ''.join([f'''
<tr id="patient-row-{p["id"]}">
<td>{p.get("id", "N/A")}</td>
<td>{p.get("name", "N/A")}</td>
<td>{p.get("phone", "N/A")}</td>
<td>{p.get("dob", "N/A")}</td>
<td>
<button class="action-btn delete-btn" onclick="deletePatient({p.get('id')})">Удалить</button>
</td>
</tr>
''' for p in sorted(patients, key=lambda x: x.get('id', 0))]) + '''
</tbody>
</table>
</div>
<div id="protocols" class="tab">
<h2>Новый протокол</h2>
<div id="protocol-selection-area">
<p>Выберите тип протокола:</p>
<div class="protocol-categories" id="protocol-selector-categories">
</div>
</div>
<form id="protocol-form" onsubmit="handleProtocolSubmit(event)" style="display: none;">
<input type="hidden" name="protocol_type" id="protocol_type_input">
<div class="form-group">
<label for="patient_select_protocol">Пациент:</label>
<select name="patient_id" id="patient_select_protocol" required>
<option value="">-- Выберите пациента --</option>
''' + ''.join([f'<option value="{p["id"]}">{p["name"]} (ID: {p["id"]})</option>'
for p in sorted(patients, key=lambda x: x.get('name', ''))]) + '''
</select>
</div>
<div class="form-group">
<label for="protocol_date">Дата исследования:</label>
<input type="date" id="protocol_date" name="protocol_date" value="''' + datetime.now().strftime('%Y-%m-%d') + '''" required>
</div>
<div id="protocol-form-container">
</div>
<button type="submit">Сохранить протокол</button>
<button type="button" onclick="cancelProtocolCreation()">Отмена</button>
</form>
<hr style="margin: 30px 0;">
<h2>Список созданных протоколов</h2>
<div class="form-section">
<div class="form-group">
<label for="protocolSearch">Поиск по пациенту или дате:</label>
<input type="text" id="protocolSearch" placeholder="Начните вводить ФИО или дату (ГГГГ-ММ-ДД)..."
onkeyup="searchProtocols(this.value)">
</div>
</div>
<table id="protocolsTable">
<thead>
<tr><th>ID</th><th>Пациент</th><th>Тип протокола</th><th>Дата</th><th>Действия</th></tr>
</thead>
<tbody>
''' + ''.join([f'''
<tr id="protocol-row-{p["id"]}">
<td>{p.get("id", "N/A")}</td>
<td>{patient_dict.get(p.get("patient_id"), "Пациент не найден")} (ID: {p.get("patient_id")})</td>
<td>{p.get("type", "N/A")}</td>
<td>{p.get("date", "N/A")}</td>
<td>
<button class="action-btn view-btn" onclick="showProtocol({p.get('id')})">Просмотр</button>
<button class="action-btn delete-btn" onclick="deleteProtocol({p.get('id')})">Удалить</button>
</td>
</tr>
''' for p in sorted(protocols, key=lambda x: x.get('id', 0), reverse=True)]) + '''
</tbody>
</table>
</div>
<div id="control" class="tab">
<h2>Добавить пациента на контроль</h2>
<div class="form-section">
<form id="addControlForm">
<div class="form-group">
<label for="patient_select_control">Пациент:</label>
<select name="patient_id" id="patient_select_control" required>
<option value="">-- Выберите пациента --</option>
''' + ''.join([f'<option value="{p["id"]}">{p["name"]} (ID: {p["id"]})</option>'
for p in sorted(patients, key=lambda x: x.get('name', ''))]) + '''
</select>
</div>
<div class="form-group">
<label for="control_reason_add">Причина контроля:</label>
<textarea name="reason" id="control_reason_add" placeholder="Опишите причину постановки на контроль" required></textarea>
</div>
<button type="submit">Добавить на контроль</button>
</form>
</div>
<h2>Пациенты на контроле</h2>
<table id="controlTable">
<thead>
<tr><th>ФИО</th><th>Причина</th><th>Дата постановки</th><th>Действия</th></tr>
</thead>
<tbody>
''' + ''.join([f'''
<tr id="control-row-{c.get("patient_id")}">
<td>{c.get("name", "N/A")}</td>
<td>{c.get("reason", "N/A")}</td>
<td>{c.get("date", "N/A")}</td>
<td>
<button class="action-btn remove-btn" onclick="removeControl({c.get('patient_id')})">Снять с контроля</button>
</td>
</tr>
''' for c in sorted(control, key=lambda x: x.get('date', ''), reverse=True)]) + '''
</tbody>
</table>
</div>
<div id="database" class="tab">
<h2>База данных пациентов и исследований</h2>
<div class="form-section">
<div class="form-group">
<label for="search">Поиск по ФИО:</label>
<input type="text" id="search" placeholder="Начните вводить ФИО..."
onkeyup="searchDatabase(this.value)">
</div>
</div>
<table id="dbTable">
<thead>
<tr><th>ФИО</th><th>Телефон</th><th>Год рожд.</th><th>Протоколы исследований</th></tr>
</thead>
<tbody>
<tr><td colspan="4">Загрузка...</td></tr>
</tbody>
</table>
</div>
<div id="admin" class="tab">
<h2>Администрирование</h2>
<div class="form-section">
<h3>Резервное копирование и восстановление (Hugging Face)</h3>
<p>Данные сохраняются автоматически при каждом изменении и периодически (если настроен токен записи).</p>
<p>Кнопки ниже для ручного управления:</p>
<button class="action-btn" onclick="manualBackup()" ''' + ("disabled" if not HF_TOKEN_WRITE else "") + '''>Создать бэкап сейчас</button>
<button class="action-btn" onclick="manualDownload()" ''' + ("disabled" if not HF_TOKEN_READ else "") + '''>Скачать бэкап с сервера</button>
<p id="admin-message" style="margin-top: 10px; font-weight: bold;"></p>
</div>
</div>
</div>
<div id="protocolModal" style="display: none;">
<div class="overlay" onclick="closeModal()"></div>
<div class="modal">
<h3 id="modalTitle">Просмотр протокола</h3>
<div id="modalBody">
</div>
<button type="button" class="action-btn" onclick="openProtocolForPrint()" style="margin-right: 10px;">Печать</button>
<button type="button" class="close-modal-btn" onclick="closeModal()">Закрыть</button>
</div>
</div>
<script>
function showTab(tabId) {
document.querySelectorAll('.tab').forEach(tab => tab.style.display = 'none');
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
const currentTab = document.getElementById(tabId);
if (currentTab) {
currentTab.style.display = 'block';
currentTab.classList.add('active');
}
const currentBtn = document.querySelector(`.tab-btn[onclick="showTab('${tabId}')"]`);
if (currentBtn) {
currentBtn.classList.add('active');
}
if (tabId !== 'protocols') {
cancelProtocolCreation();
}
if (tabId === 'database') {
searchDatabase('');
}
if (tabId === 'protocols') {
searchProtocols('');
}
if (tabId === 'control') {
document.getElementById('control_reason_add').value = '';
document.getElementById('patient_select_control').selectedIndex = 0;
}
if (tabId === 'reception') {
document.getElementById('control_checkbox').checked = false;
document.getElementById('control_reason_group').style.display = 'none';
document.getElementById('control_reason').value = '';
}
}
function closeModal() {
const modal = document.getElementById('protocolModal');
if (modal) {
modal.style.display = 'none';
document.getElementById('modalBody').innerHTML = '';
currentProtocolIdForPrint = null;
}
}
function showModal(title, content) {
const modal = document.getElementById('protocolModal');
const modalTitle = document.getElementById('modalTitle');
const modalBody = document.getElementById('modalBody');
if (modal && modalTitle && modalBody) {
modalTitle.textContent = title;
modalBody.innerHTML = content;
modal.style.display = 'block';
}
}
function showLoading(elementId) {
const element = document.getElementById(elementId);
if(element) element.innerHTML = '<p>Загрузка...</p>';
else {
const parent = document.querySelector(elementId);
if(parent) parent.innerHTML = '<tr><td colspan="100%">Загрузка...</td></tr>';
}
}
function showMessage(elementId, message, isError = false) {
const element = document.getElementById(elementId);
if(element) {
element.textContent = message;
element.style.color = isError ? 'red' : 'green';
setTimeout(() => { element.textContent = ''; }, 5000);
}
}
const jsProtocolDefinitions = {
'УЗИ внутренних органов': {
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 месяцев.' }
]
},
};
function populateProtocolSelector() {
const categoriesDiv = document.getElementById('protocol-selector-categories');
if (!categoriesDiv) return;
categoriesDiv.innerHTML = '';
const protocolCategories = {};
for (const protocolName in jsProtocolDefinitions) {
const protocol = jsProtocolDefinitions[protocolName];
const categoryName = protocol.category || 'Другое';
if (!protocolCategories[categoryName]) {
protocolCategories[categoryName] = [];
}
protocolCategories[categoryName].push({ name: protocolName, label: protocolName });
}
const sortedCategories = Object.keys(protocolCategories).sort();
sortedCategories.forEach(categoryName => {
const categoryDiv = document.createElement('div');
categoryDiv.className = 'protocol-category';
categoryDiv.innerHTML = `<h3>${categoryName}</h3>`;
const buttonsDiv = document.createElement('div');
buttonsDiv.className = 'protocol-category-buttons';
protocolCategories[categoryName].sort((a, b) => a.label.localeCompare(b.label)).forEach(protocolInfo => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'protocol-select-btn';
button.textContent = protocolInfo.label;
button.onclick = () => generateProtocolForm(protocolInfo.name);
buttonsDiv.appendChild(button);
});
categoryDiv.appendChild(buttonsDiv);
categoriesDiv.appendChild(categoryDiv);
});
}
function createFieldElement(field, counter) {
const fieldId = `protocol_field_${field.name}_${counter}`;
const fieldDiv = document.createElement('div');
fieldDiv.className = 'protocol-field';
if (field.type === 'header') {
const header = document.createElement('h4');
header.textContent = field.label;
fieldDiv.appendChild(header);
return fieldDiv
}
if (field.type === 'note') {
const note = document.createElement('p');
note.textContent = field.label;
fieldDiv.appendChild(note);
return fieldDiv;
}
const label = document.createElement('label');
label.htmlFor = fieldId;
label.textContent = field.label;
fieldDiv.appendChild(label);
let inputElement;
if (field.type === 'textarea') {
inputElement = document.createElement('textarea');
inputElement.placeholder = field.placeholder || '';
if (field.rows) inputElement.rows = field.rows;
} else if (field.type === 'select') {
inputElement = document.createElement('select');
const defaultOption = document.createElement('option');
defaultOption.value = "";
defaultOption.textContent = "-- Выберите --";
inputElement.appendChild(defaultOption);
field.options.forEach(optionValue => {
const option = document.createElement('option');
option.value = optionValue;
option.textContent = optionValue;
inputElement.appendChild(option);
});
if (field.default) inputElement.value = field.default;
} else {
inputElement = document.createElement('input');
inputElement.type = field.type;
inputElement.placeholder = field.placeholder || '';
if (field.step) inputElement.step = field.step;
if (field.min) inputElement.min = field.min;
if (field.max) inputElement.max = field.max;
}
inputElement.id = fieldId;
inputElement.name = field.name;
if (field.required) inputElement.required = true;
if (field.default && field.type !== 'select') inputElement.value = field.default;
fieldDiv.appendChild(inputElement);
if (field.condition) {
fieldDiv.style.display = 'none';
fieldDiv.dataset.conditionField = field.condition.field;
fieldDiv.dataset.conditionValue = field.condition.value;
}
return fieldDiv;
}
function addConditionalDisplayListeners(formContainer) {
const conditionalFields = formContainer.querySelectorAll('[data-condition-field]');
conditionalFields.forEach(conditionalDiv => {
const triggerFieldName = conditionalDiv.dataset.conditionField;
const triggerValue = conditionalDiv.dataset.conditionValue;
const triggerElement = formContainer.querySelector(`[name="${triggerFieldName}"]`);
if (triggerElement) {
triggerElement.addEventListener('change', (event) => {
if (event.target.value === triggerValue) {
conditionalDiv.style.display = 'block';
} else {
conditionalDiv.style.display = 'none';
const inputInside = conditionalDiv.querySelector('input, select, textarea');
if(inputInside) inputInside.value = '';
}
});
if (triggerElement.value === triggerValue) {
conditionalDiv.style.display = 'block';
} else {
conditionalDiv.style.display = 'none';
const inputInside = conditionalDiv.querySelector('input, select, textarea');
if(inputInside) inputInside.value = '';
}
}
});
}
function generateProtocolForm(protocolName) {
const definition = jsProtocolDefinitions[protocolName];
if (!definition) {
alert('Ошибка: Определение для этого протокола не найдено.');
return;
}
const formContainer = document.getElementById('protocol-form-container');
const form = document.getElementById('protocol-form');
const selectionArea = document.getElementById('protocol-selection-area');
const protocolTypeInput = document.getElementById('protocol_type_input');
if (!formContainer || !form || !selectionArea || !protocolTypeInput) return;
formContainer.innerHTML = '';
protocolTypeInput.value = protocolName;
let fieldCounter = 0;
definition.fields.forEach(field => {
fieldCounter++;
const fieldElement = createFieldElement(field, fieldCounter);
formContainer.appendChild(fieldElement);
});
addConditionalDisplayListeners(formContainer);
selectionArea.style.display = 'none';
form.style.display = 'block';
}
function cancelProtocolCreation() {
const formContainer = document.getElementById('protocol-form-container');
const form = document.getElementById('protocol-form');
const selectionArea = document.getElementById('protocol-selection-area');
if (formContainer && form && selectionArea) {
formContainer.innerHTML = '';
form.style.display = 'none';
selectionArea.style.display = 'block';
form.reset();
document.getElementById('protocol_date').value = new Date().toISOString().split('T')[0];
document.getElementById('patient_select_protocol').selectedIndex = 0;
}
}
async function handleProtocolSubmit(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const submitButton = form.querySelector('button[type="submit"]');
submitButton.disabled = true;
submitButton.textContent = 'Сохранение...';
try {
const response = await fetch('/add_protocol', {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.status === 'success' && result.protocol) {
alert('Протокол успешно сохранен.');
addProtocolToTable(result.protocol);
cancelProtocolCreation();
} else {
throw new Error(result.error || 'Не удалось сохранить протокол.');
}
} catch (error) {
console.error('Ошибка при сохранении протокола:', error);
alert(`Ошибка: ${error.message}`);
} finally {
submitButton.disabled = false;
submitButton.textContent = 'Сохранить протокол';
}
}
function addProtocolToTable(protocol) {
const tableBody = document.querySelector('#protocolsTable tbody');
if (!tableBody || !protocol) return;
const patientSelect = document.getElementById('patient_select_protocol');
let patientName = `Пациент ID: ${protocol.patient_id}`;
if(patientSelect) {
const option = patientSelect.querySelector(`option[value="${protocol.patient_id}"]`);
if(option) {
patientName = option.textContent;
} else {
fetch(`/get_patient_name/${protocol.patient_id}`)
.then(response => response.json())
.then(data => {
if(data.name) {
const cell = document.querySelector(`#protocol-row-${protocol.id} td:nth-child(2)`);
if (cell) cell.textContent = `${data.name} (ID: ${protocol.patient_id})`;
}
})
.catch(err => console.error("Error fetching patient name for table:", err));
}
}
const newRow = document.createElement('tr');
newRow.id = `protocol-row-${protocol.id}`;
newRow.innerHTML = `
<td>${protocol.id}</td>
<td>${patientName}</td>
<td>${protocol.type}</td>
<td>${protocol.date}</td>
<td>
<button class="action-btn view-btn" onclick="showProtocol(${protocol.id})">Просмотр</button>
<button class="action-btn delete-btn" onclick="deleteProtocol(${protocol.id})">Удалить</button>
</td>
`;
tableBody.insertBefore(newRow, tableBody.firstChild);
}
let currentProtocolIdForPrint = null;
async function showProtocol(protocolId) {
currentProtocolIdForPrint = protocolId;
showLoading('modalBody');
showModal('Загрузка протокола...', '');
try {
const response = await fetch('/get_protocol/' + protocolId);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const protocol = await response.json();
if (!protocol || !protocol.data || !protocol.type) throw new Error('Неверный формат данных протокола');
const definition = jsProtocolDefinitions[protocol.type];
let content = `<div class="protocol-view-data">`;
content += `<p><strong>ID Протокола:</strong> ${protocol.id}</p>`;
content += `<p><strong>Пациент:</strong> ${protocol.patient_name || 'Не указан'} (ID: ${protocol.patient_id})</p>`;
content += `<p><strong>Тип протокола:</strong> ${protocol.type}</p>`;
content += `<p><strong>Дата исследования:</strong> ${protocol.date}</p>`;
content += `<hr>`;
let currentHeader = '';
const processedKeys = new Set(['patient_id', 'protocol_type', 'protocol_date', 'id', 'creation_timestamp', 'last_updated_timestamp']);
if (definition && definition.fields) {
definition.fields.forEach(fieldDef => {
processedKeys.add(fieldDef.name);
if (fieldDef.type === 'header') {
currentHeader = fieldDef.label;
content += `<h4>${currentHeader}</h4>`;
} else if (fieldDef.type !== 'note' && protocol.data.hasOwnProperty(fieldDef.name)) {
const value = protocol.data[fieldDef.name];
if (value !== null && value !== '' && !fieldDef.name.endsWith('_label') && !fieldDef.name.endsWith('_header')) {
content += `<p><strong>${fieldDef.label}:</strong> ${String(value).replace(/\\n/g, '<br>')}</p>`;
}
}
});
let hasOtherData = false;
let otherDataContent = '';
for (const key in protocol.data) {
if (!processedKeys.has(key) && !key.endsWith('_label') && !key.endsWith('_header')) {
const value = protocol.data[key];
if (value !== null && value !== '') {
if (!hasOtherData) {
otherDataContent += `<h4>Прочие данные:</h4>`;
hasOtherData = true;
}
let label = key.replace(/_/g, ' ');
label = label.charAt(0).toUpperCase() + label.slice(1);
otherDataContent += `<p><strong>${label}:</strong> ${String(value).replace(/\\n/g, '<br>')}</p>`;
}
}
}
content += otherDataContent;
} else {
content += `<h3>Данные исследования (определение не найдено):</h3>`;
for (const key in protocol.data) {
const value = protocol.data[key];
if (value !== null && value !== '' && !key.endsWith('_label') && !key.endsWith('_header')) {
let label = key.replace(/_/g, ' ');
label = label.charAt(0).toUpperCase() + label.slice(1);
content += `<p><strong>${label}:</strong> ${String(value).replace(/\\n/g, '<br>')}</p>`;
}
}
}
content += `</div>`;
showModal('Просмотр протокола', content);
} catch (error) {
console.error('Ошибка при загрузке протокола:', error);
showModal('Ошибка', `<p style="color: red;">Не удалось загрузить данные протокола. ${error.message}</p>`);
}
}
function openProtocolForPrint() {
const protocolId = currentProtocolIdForPrint;
if (protocolId) {
window.open('/print_protocol/' + protocolId, '_blank');
} else {
alert('ID протокола не определен для печати.');
}
}
async function deleteProtocol(protocolId) {
if (confirm(`Вы уверены, что хотите удалить протокол ID ${protocolId}? Это действие необратимо.`)) {
try {
const response = await fetch('/delete_protocol/' + protocolId, { method: 'POST' });
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.status === 'success') {
alert('Протокол успешно удален.');
document.getElementById(`protocol-row-${protocolId}`)?.remove();
} else {
throw new Error(result.error || 'Неизвестная ошибка при удалении.');
}
} catch (error) {
console.error('Ошибка при удалении протокола:', error);
alert(`Не удалось удалить протокол: ${error.message}`);
}
}
}
document.getElementById('addPatientForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const submitButton = this.querySelector('button[type="submit"]');
submitButton.disabled = true;
submitButton.textContent = 'Добавление...';
try {
const response = await fetch('/add_patient', { method: 'POST', body: formData });
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.status === 'success' && result.patient) {
alert('Пациент успешно добавлен.');
this.reset();
document.getElementById('control_checkbox').checked = false;
document.getElementById('control_reason_group').style.display = 'none';
addPatientToTables(result.patient, result.control_entry);
} else { throw new Error(result.error || 'Не удалось добавить пациента.'); }
} catch (error) {
console.error('Ошибка при добавлении пациента:', error);
alert(`Ошибка: ${error.message}`);
} finally {
submitButton.disabled = false;
submitButton.textContent = 'Добавить пациента';
}
});
document.getElementById('control_checkbox').addEventListener('change', function() {
const controlReasonGroup = document.getElementById('control_reason_group');
controlReasonGroup.style.display = this.checked ? 'flex' : 'none';
if (!this.checked) {
document.getElementById('control_reason').value = '';
}
});
function addPatientToTables(patient, controlEntry) {
const patientsTableBody = document.querySelector('#patientsTable tbody');
if (patientsTableBody) {
const newRow = document.createElement('tr');
newRow.id = `patient-row-${patient.id}`;
newRow.innerHTML = `
<td>${patient.id}</td>
<td>${patient.name}</td>
<td>${patient.phone}</td>
<td>${patient.dob || 'N/A'}</td>
<td><button class="action-btn delete-btn" onclick="deletePatient(${patient.id})">Удалить</button></td>`;
patientsTableBody.appendChild(newRow);
}
const protocolSelect = document.getElementById('patient_select_protocol');
const controlSelect = document.getElementById('patient_select_control');
const option = document.createElement('option');
option.value = patient.id;
option.textContent = `${patient.name} (ID: ${patient.id})`;
if (protocolSelect) {
const existingOptionP = protocolSelect.querySelector(`option[value="${patient.id}"]`);
if (!existingOptionP) protocolSelect.appendChild(option.cloneNode(true));
}
if (controlSelect) {
const existingOptionC = controlSelect.querySelector(`option[value="${patient.id}"]`);
if (!existingOptionC) controlSelect.appendChild(option.cloneNode(true));
}
if (controlEntry) {
addControlEntryToTable(controlEntry);
}
searchDatabase(document.getElementById('search').value);
searchProtocols(document.getElementById('protocolSearch').value);
}
async function deletePatient(patientId) {
if (confirm(`УДАЛИТЬ ПАЦИЕНТА ID ${patientId}? Все протоколы и записи контроля будут стерты! НЕОБРАТИМО!`)) {
try {
const response = await fetch('/delete_patient/' + patientId, { method: 'POST' });
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.status === 'success') {
alert('Пациент и все связанные данные успешно удалены.');
document.getElementById(`patient-row-${patientId}`)?.remove();
document.querySelectorAll(`#patient_select_protocol option[value="${patientId}"]`)?.forEach(o => o.remove());
document.querySelectorAll(`#patient_select_control option[value="${patientId}"]`)?.forEach(o => o.remove());
document.getElementById(`control-row-${patientId}`)?.remove();
document.querySelectorAll(`#protocolsTable tbody tr`).forEach(row => {
const patientCell = row.querySelector('td:nth-child(2)');
if (patientCell && patientCell.textContent.includes(`(ID: ${patientId})`)) {
row.remove();
}
});
searchDatabase(document.getElementById('search').value);
} else { throw new Error(result.error || 'Ошибка при удалении пациента.'); }
} catch (error) {
console.error('Ошибка при удалении пациента:', error);
alert(`Не удалось удалить пациента: ${error.message}`);
}
}
}
document.getElementById('addControlForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const submitButton = this.querySelector('button[type="submit"]');
submitButton.disabled = true;
submitButton.textContent = 'Добавление...';
try {
const response = await fetch('/add_control', { method: 'POST', body: formData });
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.status === 'success' && result.control_entry) {
alert('Пациент добавлен на контроль.');
this.reset();
addControlEntryToTable(result.control_entry);
} else { throw new Error(result.error || 'Не удалось добавить на контроль.'); }
} catch (error) {
console.error('Ошибка при добавлении на контроль:', error);
alert(`Ошибка: ${error.message}`);
} finally {
submitButton.disabled = false;
submitButton.textContent = 'Добавить на контроль';
}
});
function addControlEntryToTable(entry) {
const controlTableBody = document.querySelector('#controlTable tbody');
if (controlTableBody) {
const existingRow = document.getElementById(`control-row-${entry.patient_id}`);
if (existingRow) existingRow.remove();
const newRow = document.createElement('tr');
newRow.id = `control-row-${entry.patient_id}`;
newRow.innerHTML = `
<td>${entry.name}</td>
<td>${entry.reason}</td>
<td>${entry.date}</td>
<td><button class="action-btn remove-btn" onclick="removeControl(${entry.patient_id})">Снять с контроля</button></td>`;
controlTableBody.insertBefore(newRow, controlTableBody.firstChild);
}
}
async function removeControl(patientId) {
if (confirm(`Снять пациента (ID ${patientId}) с контроля?`)) {
try {
const response = await fetch('/remove_control/' + patientId, { method: 'POST' });
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.status === 'success') {
alert('Пациент снят с контроля.');
document.getElementById(`control-row-${patientId}`)?.remove();
} else { throw new Error(result.error || 'Ошибка при снятии с контроля.'); }
} catch (error) {
console.error('Ошибка при снятии с контроля:', error);
alert(`Не удалось снять с контроля: ${error.message}`);
}
}
}
async function searchDatabase(query) {
const tableBody = document.querySelector('#dbTable tbody');
if (!tableBody) return;
showLoading('#dbTable tbody');
try {
const response = await fetch('/search?query=' + encodeURIComponent(query));
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
tableBody.innerHTML = '';
if (data.length === 0) {
tableBody.innerHTML = '<tr><td colspan="4">Пациенты не найдены.</td></tr>';
} else {
data.forEach(p => {
const row = document.createElement('tr');
const protocolsHtml = p.protocols && p.protocols.length > 0
? p.protocols.map(proto => `<div>${proto}</div>`).join('')
: 'Нет протоколов';
row.innerHTML = `
<td>${p.name || 'N/A'}</td>
<td>${p.phone || 'N/A'}</td>
<td>${p.dob || 'N/A'}</td>
<td>${protocolsHtml}</td>`;
tableBody.appendChild(row);
});
}
} catch (error) {
console.error('Ошибка поиска:', error);
tableBody.innerHTML = `<tr><td colspan="4" style="color: red;">Ошибка при выполнении поиска: ${error.message}</td></tr>`;
}
}
function searchProtocols(query) {
const searchTerm = query.toLowerCase().trim();
const tableBody = document.querySelector('#protocolsTable tbody');
const rows = tableBody.querySelectorAll('tr');
let found = false;
rows.forEach(row => {
const patientCell = row.cells[1];
const dateCell = row.cells[3];
let match = false;
if (patientCell && patientCell.textContent.toLowerCase().includes(searchTerm)) {
match = true;
}
if (dateCell && dateCell.textContent.toLowerCase().includes(searchTerm)) {
match = true;
}
if (match) {
row.style.display = '';
found = true;
} else {
row.style.display = 'none';
}
});
let noResultsRow = tableBody.querySelector('.no-results');
if (!found && !noResultsRow) {
noResultsRow = tableBody.insertRow();
noResultsRow.className = 'no-results';
const cell = noResultsRow.insertCell();
cell.colSpan = 5;
cell.textContent = 'Протоколы не найдены.';
cell.style.textAlign = 'center';
} else if (found && noResultsRow) {
noResultsRow.remove();
}
}
async function manualBackup() {
showMessage('admin-message', 'Запуск ручного резервного копирования...');
try {
const response = await fetch('/backup', { method: 'POST' });
const text = await response.text();
if (response.ok) { showMessage('admin-message', text, false); }
else { throw new Error(text || `HTTP error! status: ${response.status}`); }
} catch (error) {
console.error('Ошибка ручного бэкапа:', error);
showMessage('admin-message', `Ошибка бэкапа: ${error.message}`, true);
}
}
async function manualDownload() {
showMessage('admin-message', 'Запуск скачивания с сервера...');
try {
const response = await fetch('/download', { method: 'GET' });
const text = await response.text();
if (response.ok) {
showMessage('admin-message', text + ' Перезагрузка страницы может потребоваться для обновления данных.', false);
} else { throw new Error(text || `HTTP error! status: ${response.status}`); }
} catch (error) {
console.error('Ошибка ручного скачивания:', error);
showMessage('admin-message', `Ошибка скачивания: ${error.message}`, true);
}
}
document.addEventListener('DOMContentLoaded', () => {
showTab('reception');
populateProtocolSelector();
});
</script>
</body>
</html>
'''
@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/<int:patient_id>', 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/<int:protocol_id>')
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/<int:patient_id>')
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/<int:protocol_id>', 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/<int:protocol_id>', 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/<int:patient_id>', 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/<int:protocol_id>')
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"<h4>{field_label}</h4>"
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', '<br>')
content_html += f"<p><strong>{field_label}:</strong> {value_display}</p>"
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"<h4>Прочие данные:</h4>"
other_data_header_added = True
label = key.replace('_', ' ').capitalize()
value_display = str(value).replace('\n', '<br>')
content_html += f"<p><strong>{label}:</strong> {value_display}</p>"
else:
content_html += f"<h4>Данные исследования:</h4>"
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', '<br>')
content_html += f"<p><strong>{label}:</strong> {value_display}</p>"
html_template = f"""
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Протокол №{protocol_id_num} - {protocol_type}</title>
<style>
body {{
font-family: 'Times New Roman', Times, serif; /* Standard serif font */
font-size: 12pt;
line-height: 1.4;
margin: 0.75in; /* Roughly standard margins */
background-color: white;
color: black;
}}
.header-block {{
display: flex;
justify-content: space-between;
font-size: 10pt;
margin-bottom: 0.2in;
padding-bottom: 0.2in;
border-bottom: 1px solid black;
}}
.header-block div {{
width: 48%;
line-height: 1.3;
}}
.header-block b {{ font-size: 11pt; }}
.protocol-title {{
text-align: center;
font-size: 14pt;
font-weight: bold;
margin-bottom: 0.2in;
}}
.patient-info p {{
margin: 0.05in 0;
font-size: 12pt;
}}
.protocol-body {{
margin-top: 0.3in;
}}
.protocol-body h4 {{
font-size: 13pt;
margin-top: 0.25in;
margin-bottom: 0.1in;
border-bottom: 1px dotted #555;
padding-bottom: 2px;
}}
.protocol-body p {{
margin: 0.08in 0;
text-indent: 0.2in; /* Optional paragraph indent */
}}
.protocol-body p strong {{
font-weight: bold;
min-width: 180px; /* Adjust as needed */
display: inline-block;
text-indent: 0; /* Reset indent for bold label */
}}
.footer {{
margin-top: 0.5in;
padding-top: 0.2in;
border-top: 1px solid black;
text-align: right;
font-size: 11pt;
}}
hr {{ display: none; }} /* Hide HR from modal view */
@media print {{
body {{ margin: 0.5in; font-size: 11pt; }} /* Smaller margins for print */
.header-block {{ font-size: 9pt; }}
.header-block b {{ font-size: 10pt; }}
.protocol-title {{ font-size: 13pt; }}
.patient-info p {{ font-size: 11pt; }}
.protocol-body h4 {{ font-size: 12pt; }}
.protocol-body p {{ font-size: 11pt; }}
.footer {{ font-size: 10pt; }}
button {{ display: none; }} /* Hide buttons when printing */
}}
.print-button {{
display: block;
margin: 20px auto;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}}
</style>
</head>
<body>
<div class="header-block">
<div>
<b>«Ырыс-Медиа» Медициналык Борбору</b><br>
Бишкек ш. Кулиев/Киев «Кенч» соода борбору 3-кабат<br>
0778 70 90 52, 0500 70 90 52<br>
Инстаграм: yrysmedcentre
</div>
<div>
<b>«Ырыс-Медиа» Медицинский центр</b><br>
г. Бишкек, ул. Кулиева/Киев, торговый центр «Кенч», 3-этаж<br>
0778 70 90 52, 0500 70 90 52<br>
Инстаграм: yrysmedcentre
</div>
</div>
<div class="protocol-title">
{protocol_type} - Протокол №{protocol_id_num}
</div>
<div class="patient-info">
<p><strong>Пациент:</strong> {patient_name}</p>
<p><strong>Год рождения:</strong> {patient_dob}</p>
<p><strong>Дата исследования:</strong> {protocol_date}</p>
</div>
<div class="protocol-body">
{content_html}
</div>
<div class="footer">
<p>Врач: ________________________</p>
<p>Дата: {datetime.now().strftime('%d.%m.%Y')}</p>
</div>
<button class="print-button" onclick="window.print()">Печатать протокол</button>
</body>
</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)