|
|
|
|
|
|
|
|
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) |