diff --git a/streamlit_app/api_client.py b/streamlit_app/api_client.py new file mode 100644 index 0000000..22b92c2 --- /dev/null +++ b/streamlit_app/api_client.py @@ -0,0 +1,80 @@ +""" +Модуль для работы с API +""" +import requests +import os +from typing import Dict, Any, List, Tuple + +# Конфигурация API +API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") # Внутренний адрес для Docker +API_PUBLIC_URL = os.getenv("API_PUBLIC_URL", "http://localhost:8000") # Внешний адрес для пользователя + + +def check_api_health() -> bool: + """Проверка доступности API""" + try: + response = requests.get(f"{API_BASE_URL}/", timeout=5) + return response.status_code == 200 + except: + return False + + +def get_available_parsers() -> List[str]: + """Получение списка доступных парсеров""" + try: + response = requests.get(f"{API_BASE_URL}/parsers") + if response.status_code == 200: + return response.json()["parsers"] + return [] + except: + return [] + + +def get_server_info() -> Dict[str, Any]: + """Получение информации о сервере""" + try: + response = requests.get(f"{API_BASE_URL}/server-info") + if response.status_code == 200: + return response.json() + return {} + except: + return {} + + +def upload_file_to_api(endpoint: str, file_data: bytes, filename: str) -> Tuple[Dict[str, Any], int]: + """Загрузка файла на API""" + try: + # Определяем правильное имя поля в зависимости от эндпоинта + if "zip" in endpoint: + files = {"zip_file": (filename, file_data, "application/zip")} + else: + files = {"file": (filename, file_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} + + response = requests.post(f"{API_BASE_URL}{endpoint}", files=files) + return response.json(), response.status_code + except Exception as e: + return {"error": str(e)}, 500 + + +def make_api_request(endpoint: str, data: Dict[str, Any]) -> Tuple[Dict[str, Any], int]: + """Выполнение API запроса""" + try: + response = requests.post(f"{API_BASE_URL}{endpoint}", json=data) + return response.json(), response.status_code + except Exception as e: + return {"error": str(e)}, 500 + + +def get_available_ogs(parser_name: str) -> List[str]: + """Получение доступных ОГ для парсера""" + try: + response = requests.get(f"{API_BASE_URL}/parsers/{parser_name}/available_ogs") + if response.status_code == 200: + data = response.json() + return data.get("available_ogs", []) + else: + print(f"⚠️ Ошибка получения ОГ: {response.status_code}") + return [] + except Exception as e: + print(f"⚠️ Ошибка при запросе ОГ: {e}") + return [] \ No newline at end of file diff --git a/streamlit_app/config.py b/streamlit_app/config.py new file mode 100644 index 0000000..bb49d58 --- /dev/null +++ b/streamlit_app/config.py @@ -0,0 +1,53 @@ +""" +Конфигурация приложения +""" +import streamlit as st + +# Конфигурация страницы +def setup_page_config(): + """Настройка конфигурации страницы Streamlit""" + st.set_page_config( + page_title="NIN Excel Parsers API Demo", + page_icon="📊", + layout="wide", + initial_sidebar_state="expanded" + ) + +# Константы для парсеров +PARSER_TABS = [ + "📊 Сводки ПМ", + "🏭 Сводки СА", + "⛽ Мониторинг топлива", + "🔧 Ремонт СА", + "📋 Статусы ремонта СА", + "⚡ Мониторинг ТЭР", + "🏭 Операционные справки" +] + +# Константы для ОГ +DEFAULT_OGS = [ + "SNPZ", "KNPZ", "ANHK", "AchNPZ", "UNPZ", "UNH", "NOV", + "NovKuybNPZ", "KuybNPZ", "CyzNPZ", "TuapsNPZ", "RNPK", + "NVNPO", "KLNPZ", "PurNP", "YANOS" +] + +# Константы для кодов строк ПМ +PM_CODES = [78, 79, 394, 395, 396, 397, 81, 82, 83, 84] + +# Константы для столбцов ПМ +PM_COLUMNS = ["БП", "ПП", "СЭБ", "Факт", "План"] + +# Константы для режимов СА +CA_MODES = ["plan", "fact", "normativ"] + +# Константы для таблиц СА +CA_TABLES = ["ТиП", "Топливо", "Потери"] + +# Константы для столбцов мониторинга топлива +FUEL_COLUMNS = ["normativ", "total", "total_1"] + +# Константы для типов ремонта +REPAIR_TYPES = ["КР", "КП", "ТР"] + +# Константы для режимов мониторинга ТЭР +TAR_MODES = ["all", "total", "last_day"] \ No newline at end of file diff --git a/streamlit_app/parsers_ui/__init__.py b/streamlit_app/parsers_ui/__init__.py new file mode 100644 index 0000000..515e575 --- /dev/null +++ b/streamlit_app/parsers_ui/__init__.py @@ -0,0 +1,3 @@ +""" +UI модули для парсеров +""" \ No newline at end of file diff --git a/streamlit_app/parsers_ui/monitoring_fuel_ui.py b/streamlit_app/parsers_ui/monitoring_fuel_ui.py new file mode 100644 index 0000000..8ee4fc1 --- /dev/null +++ b/streamlit_app/parsers_ui/monitoring_fuel_ui.py @@ -0,0 +1,91 @@ +""" +UI модуль для мониторинга топлива +""" +import streamlit as st +from api_client import upload_file_to_api, make_api_request +from config import FUEL_COLUMNS + + +def render_monitoring_fuel_tab(): + """Рендер вкладки мониторинга топлива""" + st.header("⛽ Мониторинг топлива - Полный функционал") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + uploaded_fuel = st.file_uploader( + "Выберите ZIP архив с мониторингом топлива", + type=['zip'], + key="fuel_upload" + ) + + if uploaded_fuel is not None: + if st.button("📤 Загрузить мониторинг топлива", key="upload_fuel_btn"): + with st.spinner("Загружаю файл..."): + result, status = upload_file_to_api( + "/monitoring_fuel/upload-zip", + uploaded_fuel.read(), + uploaded_fuel.name + ) + + if status == 200: + st.success(f"✅ {result.get('message', 'Файл загружен')}") + st.info(f"ID объекта: {result.get('object_id', 'N/A')}") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + + st.markdown("---") + + # Секция получения данных + st.subheader("🔍 Получение данных") + + col1, col2 = st.columns(2) + + with col1: + st.subheader("Агрегация по колонкам") + + columns_fuel = st.multiselect( + "Выберите столбцы", + FUEL_COLUMNS, + default=["normativ", "total"], + key="fuel_columns" + ) + + if st.button("🔍 Получить агрегированные данные", key="fuel_total_btn"): + if columns_fuel: + with st.spinner("Получаю данные..."): + data = { + "columns": columns_fuel + } + + result, status = make_api_request("/monitoring_fuel/get_total_by_columns", data) + + if status == 200: + st.success("✅ Данные получены") + st.json(result) + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + else: + st.warning("⚠️ Выберите столбцы") + + with col2: + st.subheader("Данные за месяц") + + month = st.selectbox( + "Выберите месяц", + [f"{i:02d}" for i in range(1, 13)], + key="fuel_month" + ) + + if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"): + with st.spinner("Получаю данные..."): + data = { + "month": month + } + + result, status = make_api_request("/monitoring_fuel/get_month_by_code", data) + + if status == 200: + st.success("✅ Данные получены") + st.json(result) + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") \ No newline at end of file diff --git a/streamlit_app/parsers_ui/monitoring_tar_ui.py b/streamlit_app/parsers_ui/monitoring_tar_ui.py new file mode 100644 index 0000000..e9ca666 --- /dev/null +++ b/streamlit_app/parsers_ui/monitoring_tar_ui.py @@ -0,0 +1,108 @@ +""" +UI модуль для мониторинга ТЭР +""" +import streamlit as st +import pandas as pd +import json +from api_client import upload_file_to_api, make_api_request +from config import TAR_MODES + + +def render_monitoring_tar_tab(): + """Рендер вкладки мониторинга ТЭР""" + st.header("⚡ Мониторинг ТЭР (Топливно-энергетических ресурсов)") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + uploaded_file = st.file_uploader( + "Выберите ZIP архив с файлами мониторинга ТЭР", + type=['zip'], + key="monitoring_tar_upload" + ) + + if uploaded_file is not None: + if st.button("📤 Загрузить файл", key="monitoring_tar_upload_btn"): + with st.spinner("Загружаем файл..."): + file_data = uploaded_file.read() + result, status_code = upload_file_to_api("/monitoring_tar/upload", file_data, uploaded_file.name) + + if status_code == 200: + st.success("✅ Файл успешно загружен!") + st.json(result) + else: + st.error(f"❌ Ошибка загрузки: {result}") + + # Секция получения данных + st.subheader("📊 Получение данных") + + # Выбор формата отображения + display_format = st.radio( + "Формат отображения:", + ["JSON", "Таблица"], + key="monitoring_tar_display_format", + horizontal=True + ) + + # Выбор режима данных + mode = st.selectbox( + "Выберите режим данных:", + TAR_MODES, + help="total - строки 'Всего' (агрегированные данные), last_day - последние строки данных, all - все данные", + key="monitoring_tar_mode" + ) + + if st.button("📊 Получить данные", key="monitoring_tar_get_data_btn"): + with st.spinner("Получаем данные..."): + # Выбираем эндпоинт в зависимости от режима + if mode == "all": + # Используем полный эндпоинт + result, status_code = make_api_request("/monitoring_tar/get_full_data", {}) + else: + # Используем фильтрованный эндпоинт + request_data = {"mode": mode} + result, status_code = make_api_request("/monitoring_tar/get_data", request_data) + + if status_code == 200 and result.get("success"): + st.success("✅ Данные успешно получены!") + + # Показываем данные + data = result.get("data", {}).get("value", {}) + if data: + st.subheader("📋 Результат:") + + # Парсим данные, если они пришли как строка + if isinstance(data, str): + try: + data = json.loads(data) + st.write("✅ JSON успешно распарсен") + except json.JSONDecodeError as e: + st.error(f"❌ Ошибка при парсинге JSON данных: {e}") + st.write("Сырые данные:", data) + return + + if display_format == "JSON": + # Отображаем как JSON + st.json(data) + else: + # Отображаем как таблицы + if isinstance(data, dict): + # Показываем данные по установкам + for installation_id, installation_data in data.items(): + with st.expander(f"🏭 {installation_id}"): + if isinstance(installation_data, dict): + # Показываем структуру данных + for data_type, type_data in installation_data.items(): + st.write(f"**{data_type}:**") + if isinstance(type_data, list) and type_data: + df = pd.DataFrame(type_data) + st.dataframe(df) + else: + st.write("Нет данных") + else: + st.write("Нет данных") + else: + st.json(data) + else: + st.info("📋 Нет данных для отображения") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") \ No newline at end of file diff --git a/streamlit_app/parsers_ui/oper_spravka_tech_pos_ui.py b/streamlit_app/parsers_ui/oper_spravka_tech_pos_ui.py new file mode 100644 index 0000000..f007bfa --- /dev/null +++ b/streamlit_app/parsers_ui/oper_spravka_tech_pos_ui.py @@ -0,0 +1,84 @@ +""" +UI модуль для операционных справок технологических позиций +""" +import streamlit as st +import pandas as pd +from api_client import upload_file_to_api, make_api_request, get_available_ogs + + +def render_oper_spravka_tech_pos_tab(): + """Рендер вкладки операционных справок технологических позиций""" + st.header("🏭 Операционные справки технологических позиций") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + + uploaded_file = st.file_uploader( + "Выберите ZIP архив с файлами операционных справок", + type=['zip'], + key="oper_spravka_tech_pos_upload" + ) + + if uploaded_file is not None: + if st.button("📤 Загрузить файл", key="oper_spravka_tech_pos_upload_btn"): + with st.spinner("Загружаем файл..."): + file_data = uploaded_file.read() + result, status_code = upload_file_to_api("/oper_spravka_tech_pos/upload", file_data, uploaded_file.name) + + if status_code == 200: + st.success("✅ Файл успешно загружен!") + st.json(result) + else: + st.error(f"❌ Ошибка загрузки: {result}") + + st.markdown("---") + + # Секция получения данных + st.subheader("📊 Получение данных") + + # Выбор формата отображения + display_format = st.radio( + "Формат отображения:", + ["JSON", "Таблица"], + key="oper_spravka_tech_pos_display_format", + horizontal=True + ) + + # Получаем доступные ОГ динамически + available_ogs = get_available_ogs("oper_spravka_tech_pos") + + # Выбор ОГ + og_id = st.selectbox( + "Выберите ОГ:", + available_ogs if available_ogs else ["SNPZ", "KNPZ", "ANHK", "BASH", "UNH", "NOV"], + key="oper_spravka_tech_pos_og_id" + ) + + if st.button("📊 Получить данные", key="oper_spravka_tech_pos_get_data_btn"): + with st.spinner("Получаем данные..."): + request_data = {"id": og_id} + result, status_code = make_api_request("/oper_spravka_tech_pos/get_data", request_data) + + if status_code == 200 and result.get("success"): + st.success("✅ Данные успешно получены!") + + # Показываем данные + data = result.get("data", []) + + if data and len(data) > 0: + st.subheader("📋 Результат:") + + if display_format == "JSON": + # Отображаем как JSON + st.json(data) + else: + # Отображаем как таблицу + if isinstance(data, list) and data: + df = pd.DataFrame(data) + st.dataframe(df, use_container_width=True) + else: + st.write("Нет данных") + else: + st.info("📋 Нет данных для отображения") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") \ No newline at end of file diff --git a/streamlit_app/parsers_ui/statuses_repair_ca_ui.py b/streamlit_app/parsers_ui/statuses_repair_ca_ui.py new file mode 100644 index 0000000..ac2f680 --- /dev/null +++ b/streamlit_app/parsers_ui/statuses_repair_ca_ui.py @@ -0,0 +1,146 @@ +""" +UI модуль для статусов ремонта СА +""" +import streamlit as st +import pandas as pd +from api_client import upload_file_to_api, make_api_request, get_available_ogs + + +def render_statuses_repair_ca_tab(): + """Рендер вкладки статусов ремонта СА""" + st.header("📋 Статусы ремонта СА") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + uploaded_file = st.file_uploader( + "Выберите файл статусов ремонта СА", + type=['xlsx', 'xlsm', 'xls', 'zip'], + key="statuses_repair_ca_upload" + ) + + if uploaded_file is not None: + if st.button("📤 Загрузить файл", key="statuses_repair_ca_upload_btn"): + with st.spinner("Загружаем файл..."): + file_data = uploaded_file.read() + result, status_code = upload_file_to_api("/statuses_repair_ca/upload", file_data, uploaded_file.name) + + if status_code == 200: + st.success("✅ Файл успешно загружен!") + st.json(result) + else: + st.error(f"❌ Ошибка загрузки: {result}") + + # Секция получения данных + st.subheader("📊 Получение данных") + + # Получаем доступные ОГ динамически + available_ogs = get_available_ogs("statuses_repair_ca") + + # Фильтр по ОГ + og_ids = st.multiselect( + "Выберите ОГ (оставьте пустым для всех)", + available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback + key="statuses_repair_ca_og_ids" + ) + + # Предустановленные ключи для извлечения + st.subheader("🔑 Ключи для извлечения данных") + + # Основные ключи + include_basic_keys = st.checkbox("Основные данные", value=True, key="statuses_basic_keys") + include_readiness_keys = st.checkbox("Готовность к КР", value=True, key="statuses_readiness_keys") + include_contract_keys = st.checkbox("Заключение договоров", value=True, key="statuses_contract_keys") + include_supply_keys = st.checkbox("Поставка МТР", value=True, key="statuses_supply_keys") + + # Формируем ключи на основе выбора + keys = [] + if include_basic_keys: + keys.append(["Дата начала ремонта"]) + keys.append(["Отставание / опережение подготовки к КР", "Отставание / опережение"]) + keys.append(["Отставание / опережение подготовки к КР", "Динамика за прошедшую неделю"]) + + if include_readiness_keys: + keys.append(["Готовность к КР", "Факт"]) + + if include_contract_keys: + keys.append(["Заключение договоров на СМР", "Договор", "%"]) + + if include_supply_keys: + keys.append(["Поставка МТР", "На складе, позиций", "%"]) + + # Кнопка получения данных + if st.button("📊 Получить данные", key="statuses_repair_ca_get_data_btn"): + if not keys: + st.warning("⚠️ Выберите хотя бы одну группу ключей для извлечения") + else: + with st.spinner("Получаем данные..."): + request_data = { + "ids": og_ids if og_ids else None, + "keys": keys + } + + result, status_code = make_api_request("/statuses_repair_ca/get_data", request_data) + + if status_code == 200 and result.get("success"): + st.success("✅ Данные успешно получены!") + + data = result.get("data", {}).get("value", []) + if data: + # Отображаем данные в виде таблицы + if isinstance(data, list) and len(data) > 0: + # Преобразуем в DataFrame для лучшего отображения + df_data = [] + for item in data: + row = { + "ID": item.get("id", ""), + "Название": item.get("name", ""), + } + + # Добавляем основные поля + if "Дата начала ремонта" in item: + row["Дата начала ремонта"] = item["Дата начала ремонта"] + + # Добавляем готовность к КР + if "Готовность к КР" in item: + readiness = item["Готовность к КР"] + if isinstance(readiness, dict) and "Факт" in readiness: + row["Готовность к КР (Факт)"] = readiness["Факт"] + + # Добавляем отставание/опережение + if "Отставание / опережение подготовки к КР" in item: + delay = item["Отставание / опережение подготовки к КР"] + if isinstance(delay, dict): + if "Отставание / опережение" in delay: + row["Отставание/опережение"] = delay["Отставание / опережение"] + if "Динамика за прошедшую неделю" in delay: + row["Динамика за неделю"] = delay["Динамика за прошедшую неделю"] + + # Добавляем договоры + if "Заключение договоров на СМР" in item: + contracts = item["Заключение договоров на СМР"] + if isinstance(contracts, dict) and "Договор" in contracts: + contract = contracts["Договор"] + if isinstance(contract, dict) and "%" in contract: + row["Договоры (%)"] = contract["%"] + + # Добавляем поставки МТР + if "Поставка МТР" in item: + supply = item["Поставка МТР"] + if isinstance(supply, dict) and "На складе, позиций" in supply: + warehouse = supply["На складе, позиций"] + if isinstance(warehouse, dict) and "%" in warehouse: + row["МТР на складе (%)"] = warehouse["%"] + + df_data.append(row) + + if df_data: + df = pd.DataFrame(df_data) + st.dataframe(df, use_container_width=True) + else: + st.info("📋 Нет данных для отображения") + else: + st.json(result) + else: + st.info("📋 Нет данных для отображения") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") \ No newline at end of file diff --git a/streamlit_app/parsers_ui/svodka_ca_ui.py b/streamlit_app/parsers_ui/svodka_ca_ui.py new file mode 100644 index 0000000..c451362 --- /dev/null +++ b/streamlit_app/parsers_ui/svodka_ca_ui.py @@ -0,0 +1,80 @@ +""" +UI модуль для парсера сводок СА +""" +import streamlit as st +import requests +from api_client import make_api_request, API_BASE_URL +from config import CA_MODES, CA_TABLES + + +def render_svodka_ca_tab(): + """Рендер вкладки сводок СА""" + st.header("🏭 Сводки СА - Полный функционал") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + uploaded_ca = st.file_uploader( + "Выберите Excel файл сводки СА", + type=['xlsx', 'xlsm', 'xls'], + key="ca_upload" + ) + + if uploaded_ca is not None: + if st.button("📤 Загрузить сводку СА", key="upload_ca_btn"): + with st.spinner("Загружаю файл..."): + try: + files = {"file": (uploaded_ca.name, uploaded_ca.read(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} + response = requests.post(f"{API_BASE_URL}/svodka_ca/upload", files=files) + result = response.json() + + if response.status_code == 200: + st.success(f"✅ {result.get('message', 'Файл загружен')}") + st.info(f"ID объекта: {result.get('object_id', 'N/A')}") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + except Exception as e: + st.error(f"❌ Ошибка: {str(e)}") + + st.markdown("---") + + # Секция получения данных + st.subheader("🔍 Получение данных") + + col1, col2 = st.columns(2) + + with col1: + st.subheader("Параметры запроса") + + modes = st.multiselect( + "Выберите режимы", + CA_MODES, + default=["plan", "fact"], + key="ca_modes" + ) + + tables = st.multiselect( + "Выберите таблицы", + CA_TABLES, + default=["ТиП", "Топливо"], + key="ca_tables" + ) + + with col2: + st.subheader("Результат") + if st.button("🔍 Получить данные СА", key="ca_btn"): + if modes and tables: + with st.spinner("Получаю данные..."): + data = { + "modes": modes, + "tables": tables + } + + result, status = make_api_request("/svodka_ca/get_data", data) + + if status == 200: + st.success("✅ Данные получены") + st.json(result) + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + else: + st.warning("⚠️ Выберите режимы и таблицы") \ No newline at end of file diff --git a/streamlit_app/parsers_ui/svodka_pm_ui.py b/streamlit_app/parsers_ui/svodka_pm_ui.py new file mode 100644 index 0000000..df0007c --- /dev/null +++ b/streamlit_app/parsers_ui/svodka_pm_ui.py @@ -0,0 +1,118 @@ +""" +UI модуль для парсера сводок ПМ +""" +import streamlit as st +from api_client import upload_file_to_api, make_api_request +from config import PM_CODES, PM_COLUMNS, DEFAULT_OGS + + +def render_svodka_pm_tab(): + """Рендер вкладки сводок ПМ""" + st.header("📊 Сводки ПМ - Полный функционал") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + uploaded_pm = st.file_uploader( + "Выберите ZIP архив со сводками ПМ", + type=['zip'], + key="pm_upload" + ) + + if uploaded_pm is not None: + if st.button("📤 Загрузить сводки ПМ", key="upload_pm_btn"): + with st.spinner("Загружаю файл..."): + result, status = upload_file_to_api( + "/svodka_pm/upload-zip", + uploaded_pm.read(), + uploaded_pm.name + ) + + if status == 200: + st.success(f"✅ {result.get('message', 'Файл загружен')}") + st.info(f"ID объекта: {result.get('object_id', 'N/A')}") + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + + st.markdown("---") + + # Секция получения данных + st.subheader("🔍 Получение данных") + + col1, col2 = st.columns(2) + + with col1: + st.subheader("Данные по одному ОГ") + + og_id = st.selectbox( + "Выберите ОГ", + DEFAULT_OGS, + key="pm_single_og" + ) + + codes = st.multiselect( + "Выберите коды строк", + PM_CODES, + default=[78, 79], + key="pm_single_codes" + ) + + columns = st.multiselect( + "Выберите столбцы", + PM_COLUMNS, + default=["БП", "ПП"], + key="pm_single_columns" + ) + + if st.button("🔍 Получить данные по ОГ", key="pm_single_btn"): + if codes and columns: + with st.spinner("Получаю данные..."): + data = { + "id": og_id, + "codes": codes, + "columns": columns + } + + result, status = make_api_request("/svodka_pm/get_single_og", data) + + if status == 200: + st.success("✅ Данные получены") + st.json(result) + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + else: + st.warning("⚠️ Выберите коды и столбцы") + + with col2: + st.subheader("Данные по всем ОГ") + + codes_total = st.multiselect( + "Выберите коды строк", + PM_CODES, + default=[78, 79, 394, 395], + key="pm_total_codes" + ) + + columns_total = st.multiselect( + "Выберите столбцы", + PM_COLUMNS, + default=["БП", "ПП", "СЭБ"], + key="pm_total_columns" + ) + + if st.button("🔍 Получить данные по всем ОГ", key="pm_total_btn"): + if codes_total and columns_total: + with st.spinner("Получаю данные..."): + data = { + "codes": codes_total, + "columns": columns_total + } + + result, status = make_api_request("/svodka_pm/get_total_ogs", data) + + if status == 200: + st.success("✅ Данные получены") + st.json(result) + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + else: + st.warning("⚠️ Выберите коды и столбцы") \ No newline at end of file diff --git a/streamlit_app/parsers_ui/svodka_repair_ca_ui.py b/streamlit_app/parsers_ui/svodka_repair_ca_ui.py new file mode 100644 index 0000000..944acde --- /dev/null +++ b/streamlit_app/parsers_ui/svodka_repair_ca_ui.py @@ -0,0 +1,110 @@ +""" +UI модуль для ремонта СА +""" +import streamlit as st +import pandas as pd +from api_client import upload_file_to_api, make_api_request, get_available_ogs +from config import REPAIR_TYPES + + +def render_svodka_repair_ca_tab(): + """Рендер вкладки ремонта СА""" + st.header("🔧 Ремонт СА - Управление ремонтными работами") + + # Секция загрузки файлов + st.subheader("📤 Загрузка файлов") + + uploaded_file = st.file_uploader( + "Выберите Excel файл или ZIP архив с данными о ремонте СА", + type=['xlsx', 'xlsm', 'xls', 'zip'], + key="repair_ca_upload" + ) + + if uploaded_file is not None: + if st.button("📤 Загрузить файл", key="repair_ca_upload_btn"): + with st.spinner("Загружаю файл..."): + file_data = uploaded_file.read() + result, status = upload_file_to_api("/svodka_repair_ca/upload", file_data, uploaded_file.name) + + if status == 200: + st.success("✅ Файл успешно загружен") + st.json(result) + else: + st.error(f"❌ Ошибка загрузки: {result.get('message', 'Неизвестная ошибка')}") + + st.markdown("---") + + # Секция получения данных + st.subheader("🔍 Получение данных") + + col1, col2 = st.columns(2) + + with col1: + st.subheader("Фильтры") + + # Получаем доступные ОГ динамически + available_ogs = get_available_ogs("svodka_repair_ca") + + # Фильтр по ОГ + og_ids = st.multiselect( + "Выберите ОГ (оставьте пустым для всех)", + available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback + key="repair_ca_og_ids" + ) + + # Фильтр по типам ремонта + repair_types = st.multiselect( + "Выберите типы ремонта (оставьте пустым для всех)", + REPAIR_TYPES, + key="repair_ca_types" + ) + + # Включение плановых/фактических данных + include_planned = st.checkbox("Включать плановые данные", value=True, key="repair_ca_planned") + include_factual = st.checkbox("Включать фактические данные", value=True, key="repair_ca_factual") + + with col2: + st.subheader("Действия") + + if st.button("🔍 Получить данные о ремонте", key="repair_ca_get_btn"): + with st.spinner("Получаю данные..."): + data = { + "include_planned": include_planned, + "include_factual": include_factual + } + + # Добавляем фильтры только если они выбраны + if og_ids: + data["og_ids"] = og_ids + if repair_types: + data["repair_types"] = repair_types + + result, status = make_api_request("/svodka_repair_ca/get_data", data) + + if status == 200: + st.success("✅ Данные получены") + + # Отображаем данные в виде таблицы, если возможно + if result.get("data") and isinstance(result["data"], list): + df_data = [] + for item in result["data"]: + df_data.append({ + "ID ОГ": item.get("id", ""), + "Наименование": item.get("name", ""), + "Тип ремонта": item.get("type", ""), + "Дата начала": item.get("start_date", ""), + "Дата окончания": item.get("end_date", ""), + "План": item.get("plan", ""), + "Факт": item.get("fact", ""), + "Простой": item.get("downtime", "") + }) + + if df_data: + df = pd.DataFrame(df_data) + st.dataframe(df, use_container_width=True) + else: + st.info("📋 Нет данных для отображения") + else: + st.json(result) + else: + st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") \ No newline at end of file diff --git a/streamlit_app/sidebar.py b/streamlit_app/sidebar.py new file mode 100644 index 0000000..557e7e9 --- /dev/null +++ b/streamlit_app/sidebar.py @@ -0,0 +1,53 @@ +""" +Модуль для сайдбара +""" +import streamlit as st +from api_client import get_server_info, get_available_parsers, API_PUBLIC_URL + + +def render_sidebar(): + """Рендер боковой панели""" + with st.sidebar: + st.header("ℹ️ Информация") + + # Информация о сервере + server_info = get_server_info() + if server_info: + st.subheader("Сервер") + st.write(f"PID: {server_info.get('process_id', 'N/A')}") + st.write(f"CPU ядер: {server_info.get('cpu_cores', 'N/A')}") + st.write(f"Память: {server_info.get('memory_mb', 'N/A'):.1f} MB") + + # Доступные парсеры + parsers = get_available_parsers() + if parsers: + st.subheader("Доступные парсеры") + for parser in parsers: + st.write(f"• {parser}") + + +def render_footer(): + """Рендер футера""" + st.markdown("---") + st.markdown("### 📚 Документация API") + st.markdown(f"Полная документация доступна по адресу: {API_PUBLIC_URL}/docs") + + # Информация о проекте + with st.expander("ℹ️ О проекте"): + st.markdown(""" + **NIN Excel Parsers API** - это веб-сервис для парсинга и обработки Excel-файлов нефтеперерабатывающих заводов. + + **Возможности:** + - 📊 Парсинг сводок ПМ (план и факт) + - 🏭 Парсинг сводок СА + - ⛽ Мониторинг топлива + - ⚡ Мониторинг ТЭР (Топливно-энергетические ресурсы) + - 🔧 Управление ремонтными работами СА + - 📋 Мониторинг статусов ремонта СА + + **Технологии:** + - FastAPI + - Pandas + - MinIO (S3-совместимое хранилище) + - Streamlit (веб-интерфейс) + """) \ No newline at end of file diff --git a/streamlit_app/streamlit_app.py b/streamlit_app/streamlit_app.py index 22513ab..103e878 100644 --- a/streamlit_app/streamlit_app.py +++ b/streamlit_app/streamlit_app.py @@ -1,87 +1,17 @@ import streamlit as st -import requests -import json -import pandas as pd -import io -import zipfile -from typing import Dict, Any, List -import os +from config import setup_page_config, PARSER_TABS, API_PUBLIC_URL +from api_client import check_api_health +from sidebar import render_sidebar, render_footer +from parsers_ui.svodka_pm_ui import render_svodka_pm_tab +from parsers_ui.svodka_ca_ui import render_svodka_ca_tab +from parsers_ui.monitoring_fuel_ui import render_monitoring_fuel_tab +from parsers_ui.svodka_repair_ca_ui import render_svodka_repair_ca_tab +from parsers_ui.statuses_repair_ca_ui import render_statuses_repair_ca_tab +from parsers_ui.monitoring_tar_ui import render_monitoring_tar_tab +from parsers_ui.oper_spravka_tech_pos_ui import render_oper_spravka_tech_pos_tab # Конфигурация страницы -st.set_page_config( - page_title="NIN Excel Parsers API Demo", - page_icon="📊", - layout="wide", - initial_sidebar_state="expanded" -) - -# Конфигурация API -API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") # Внутренний адрес для Docker -API_PUBLIC_URL = os.getenv("API_PUBLIC_URL", "http://localhost:8000") # Внешний адрес для пользователя - -def check_api_health(): - """Проверка доступности API""" - try: - response = requests.get(f"{API_BASE_URL}/", timeout=5) - return response.status_code == 200 - except: - return False - -def get_available_parsers(): - """Получение списка доступных парсеров""" - try: - response = requests.get(f"{API_BASE_URL}/parsers") - if response.status_code == 200: - return response.json()["parsers"] - return [] - except: - return [] - -def get_server_info(): - """Получение информации о сервере""" - try: - response = requests.get(f"{API_BASE_URL}/server-info") - if response.status_code == 200: - return response.json() - return {} - except: - return {} - -def upload_file_to_api(endpoint: str, file_data: bytes, filename: str): - """Загрузка файла на API""" - try: - # Определяем правильное имя поля в зависимости от эндпоинта - if "zip" in endpoint: - files = {"zip_file": (filename, file_data, "application/zip")} - else: - files = {"file": (filename, file_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} - - response = requests.post(f"{API_BASE_URL}{endpoint}", files=files) - return response.json(), response.status_code - except Exception as e: - return {"error": str(e)}, 500 - -def make_api_request(endpoint: str, data: Dict[str, Any]): - """Выполнение API запроса""" - try: - response = requests.post(f"{API_BASE_URL}{endpoint}", json=data) - return response.json(), response.status_code - except Exception as e: - return {"error": str(e)}, 500 - -def get_available_ogs(parser_name: str) -> List[str]: - """Получение доступных ОГ для парсера""" - try: - response = requests.get(f"{API_BASE_URL}/parsers/{parser_name}/available_ogs") - if response.status_code == 200: - data = response.json() - return data.get("available_ogs", []) - else: - print(f"⚠️ Ошибка получения ОГ: {response.status_code}") - return [] - except Exception as e: - print(f"⚠️ Ошибка при запросе ОГ: {e}") - return [] +setup_page_config() def main(): st.title("🚀 NIN Excel Parsers API - Демонстрация") @@ -89,759 +19,48 @@ def main(): # Проверка доступности API if not check_api_health(): - st.error(f"❌ API недоступен по адресу {API_BASE_URL}") + st.error(f"❌ API недоступен по адресу {API_PUBLIC_URL}") st.info("Убедитесь, что FastAPI сервер запущен") return st.success(f"✅ API доступен по адресу {API_PUBLIC_URL}") # Боковая панель с информацией - with st.sidebar: - st.header("ℹ️ Информация") - - # Информация о сервере - server_info = get_server_info() - if server_info: - st.subheader("Сервер") - st.write(f"PID: {server_info.get('process_id', 'N/A')}") - st.write(f"CPU ядер: {server_info.get('cpu_cores', 'N/A')}") - st.write(f"Память: {server_info.get('memory_mb', 'N/A'):.1f} MB") - - # Доступные парсеры - parsers = get_available_parsers() - if parsers: - st.subheader("Доступные парсеры") - for parser in parsers: - st.write(f"• {parser}") + render_sidebar() # Основные вкладки - по одной на каждый парсер - tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs([ - "📊 Сводки ПМ", - "🏭 Сводки СА", - "⛽ Мониторинг топлива", - "🔧 Ремонт СА", - "📋 Статусы ремонта СА", - "⚡ Мониторинг ТЭР", - "🏭 Операционные справки" - ]) + tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs(PARSER_TABS) # Вкладка 1: Сводки ПМ - полный функционал with tab1: - st.header("📊 Сводки ПМ - Полный функционал") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - uploaded_pm = st.file_uploader( - "Выберите ZIP архив со сводками ПМ", - type=['zip'], - key="pm_upload" - ) - - if uploaded_pm is not None: - if st.button("📤 Загрузить сводки ПМ", key="upload_pm_btn"): - with st.spinner("Загружаю файл..."): - result, status = upload_file_to_api( - "/svodka_pm/upload-zip", - uploaded_pm.read(), - uploaded_pm.name - ) - - if status == 200: - st.success(f"✅ {result.get('message', 'Файл загружен')}") - st.info(f"ID объекта: {result.get('object_id', 'N/A')}") - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - - st.markdown("---") - - # Секция получения данных - st.subheader("🔍 Получение данных") - - col1, col2 = st.columns(2) - - with col1: - st.subheader("Данные по одному ОГ") - - og_id = st.selectbox( - "Выберите ОГ", - ["SNPZ", "KNPZ", "ANHK", "AchNPZ", "UNPZ", "UNH", "NOV", - "NovKuybNPZ", "KuybNPZ", "CyzNPZ", "TuapsNPZ", "RNPK", - "NVNPO", "KLNPZ", "PurNP", "YANOS"], - key="pm_single_og" - ) - - codes = st.multiselect( - "Выберите коды строк", - [78, 79, 394, 395, 396, 397, 81, 82, 83, 84], - default=[78, 79], - key="pm_single_codes" - ) - - columns = st.multiselect( - "Выберите столбцы", - ["БП", "ПП", "СЭБ", "Факт", "План"], - default=["БП", "ПП"], - key="pm_single_columns" - ) - - if st.button("🔍 Получить данные по ОГ", key="pm_single_btn"): - if codes and columns: - with st.spinner("Получаю данные..."): - data = { - "id": og_id, - "codes": codes, - "columns": columns - } - - result, status = make_api_request("/svodka_pm/get_single_og", data) - - if status == 200: - st.success("✅ Данные получены") - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - else: - st.warning("⚠️ Выберите коды и столбцы") - - with col2: - st.subheader("Данные по всем ОГ") - - codes_total = st.multiselect( - "Выберите коды строк", - [78, 79, 394, 395, 396, 397, 81, 82, 83, 84], - default=[78, 79, 394, 395], - key="pm_total_codes" - ) - - columns_total = st.multiselect( - "Выберите столбцы", - ["БП", "ПП", "СЭБ", "Факт", "План"], - default=["БП", "ПП", "СЭБ"], - key="pm_total_columns" - ) - - if st.button("🔍 Получить данные по всем ОГ", key="pm_total_btn"): - if codes_total and columns_total: - with st.spinner("Получаю данные..."): - data = { - "codes": codes_total, - "columns": columns_total - } - - result, status = make_api_request("/svodka_pm/get_total_ogs", data) - - if status == 200: - st.success("✅ Данные получены") - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - else: - st.warning("⚠️ Выберите коды и столбцы") + render_svodka_pm_tab() # Вкладка 2: Сводки СА - полный функционал with tab2: - st.header("🏭 Сводки СА - Полный функционал") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - uploaded_ca = st.file_uploader( - "Выберите Excel файл сводки СА", - type=['xlsx', 'xlsm', 'xls'], - key="ca_upload" - ) - - if uploaded_ca is not None: - if st.button("📤 Загрузить сводку СА", key="upload_ca_btn"): - with st.spinner("Загружаю файл..."): - try: - files = {"file": (uploaded_ca.name, uploaded_ca.read(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} - response = requests.post(f"{API_BASE_URL}/svodka_ca/upload", files=files) - result = response.json() - - if response.status_code == 200: - st.success(f"✅ {result.get('message', 'Файл загружен')}") - st.info(f"ID объекта: {result.get('object_id', 'N/A')}") - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - except Exception as e: - st.error(f"❌ Ошибка: {str(e)}") - - st.markdown("---") - - # Секция получения данных - st.subheader("🔍 Получение данных") - - col1, col2 = st.columns(2) - - with col1: - st.subheader("Параметры запроса") - - modes = st.multiselect( - "Выберите режимы", - ["plan", "fact", "normativ"], - default=["plan", "fact"], - key="ca_modes" - ) - - tables = st.multiselect( - "Выберите таблицы", - ["ТиП", "Топливо", "Потери"], - default=["ТиП", "Топливо"], - key="ca_tables" - ) - - with col2: - st.subheader("Результат") - if st.button("🔍 Получить данные СА", key="ca_btn"): - if modes and tables: - with st.spinner("Получаю данные..."): - data = { - "modes": modes, - "tables": tables - } - - result, status = make_api_request("/svodka_ca/get_data", data) - - if status == 200: - st.success("✅ Данные получены") - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - else: - st.warning("⚠️ Выберите режимы и таблицы") + render_svodka_ca_tab() # Вкладка 3: Мониторинг топлива - полный функционал with tab3: - st.header("⛽ Мониторинг топлива - Полный функционал") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - uploaded_fuel = st.file_uploader( - "Выберите ZIP архив с мониторингом топлива", - type=['zip'], - key="fuel_upload" - ) - - if uploaded_fuel is not None: - if st.button("📤 Загрузить мониторинг топлива", key="upload_fuel_btn"): - with st.spinner("Загружаю файл..."): - result, status = upload_file_to_api( - "/monitoring_fuel/upload-zip", - uploaded_fuel.read(), - uploaded_fuel.name - ) - - if status == 200: - st.success(f"✅ {result.get('message', 'Файл загружен')}") - st.info(f"ID объекта: {result.get('object_id', 'N/A')}") - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - - st.markdown("---") - - # Секция получения данных - st.subheader("🔍 Получение данных") - - col1, col2 = st.columns(2) - - with col1: - st.subheader("Агрегация по колонкам") - - columns_fuel = st.multiselect( - "Выберите столбцы", - ["normativ", "total", "total_1"], - default=["normativ", "total"], - key="fuel_columns" - ) - - if st.button("🔍 Получить агрегированные данные", key="fuel_total_btn"): - if columns_fuel: - with st.spinner("Получаю данные..."): - data = { - "columns": columns_fuel - } - - result, status = make_api_request("/monitoring_fuel/get_total_by_columns", data) - - if status == 200: - st.success("✅ Данные получены") - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - else: - st.warning("⚠️ Выберите столбцы") - - with col2: - st.subheader("Данные за месяц") - - month = st.selectbox( - "Выберите месяц", - [f"{i:02d}" for i in range(1, 13)], - key="fuel_month" - ) - - if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"): - with st.spinner("Получаю данные..."): - data = { - "month": month - } - - result, status = make_api_request("/monitoring_fuel/get_month_by_code", data) - - if status == 200: - st.success("✅ Данные получены") - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + render_monitoring_fuel_tab() # Вкладка 4: Ремонт СА with tab4: - st.header("🔧 Ремонт СА - Управление ремонтными работами") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - - uploaded_file = st.file_uploader( - "Выберите Excel файл или ZIP архив с данными о ремонте СА", - type=['xlsx', 'xlsm', 'xls', 'zip'], - key="repair_ca_upload" - ) - - if uploaded_file is not None: - if st.button("📤 Загрузить файл", key="repair_ca_upload_btn"): - with st.spinner("Загружаю файл..."): - file_data = uploaded_file.read() - result, status = upload_file_to_api("/svodka_repair_ca/upload", file_data, uploaded_file.name) - - if status == 200: - st.success("✅ Файл успешно загружен") - st.json(result) - else: - st.error(f"❌ Ошибка загрузки: {result.get('message', 'Неизвестная ошибка')}") - - st.markdown("---") - - # Секция получения данных - st.subheader("🔍 Получение данных") - - col1, col2 = st.columns(2) - - with col1: - st.subheader("Фильтры") - - # Получаем доступные ОГ динамически - available_ogs = get_available_ogs("svodka_repair_ca") - - # Фильтр по ОГ - og_ids = st.multiselect( - "Выберите ОГ (оставьте пустым для всех)", - available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback - key="repair_ca_og_ids" - ) - - # Фильтр по типам ремонта - repair_types = st.multiselect( - "Выберите типы ремонта (оставьте пустым для всех)", - ["КР", "КП", "ТР"], - key="repair_ca_types" - ) - - # Включение плановых/фактических данных - include_planned = st.checkbox("Включать плановые данные", value=True, key="repair_ca_planned") - include_factual = st.checkbox("Включать фактические данные", value=True, key="repair_ca_factual") - - with col2: - st.subheader("Действия") - - if st.button("🔍 Получить данные о ремонте", key="repair_ca_get_btn"): - with st.spinner("Получаю данные..."): - data = { - "include_planned": include_planned, - "include_factual": include_factual - } - - # Добавляем фильтры только если они выбраны - if og_ids: - data["og_ids"] = og_ids - if repair_types: - data["repair_types"] = repair_types - - result, status = make_api_request("/svodka_repair_ca/get_data", data) - - if status == 200: - st.success("✅ Данные получены") - - # Отображаем данные в виде таблицы, если возможно - if result.get("data") and isinstance(result["data"], list): - df_data = [] - for item in result["data"]: - df_data.append({ - "ID ОГ": item.get("id", ""), - "Наименование": item.get("name", ""), - "Тип ремонта": item.get("type", ""), - "Дата начала": item.get("start_date", ""), - "Дата окончания": item.get("end_date", ""), - "План": item.get("plan", ""), - "Факт": item.get("fact", ""), - "Простой": item.get("downtime", "") - }) - - if df_data: - df = pd.DataFrame(df_data) - st.dataframe(df, use_container_width=True) - else: - st.info("📋 Нет данных для отображения") - else: - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + render_svodka_repair_ca_tab() # Вкладка 5: Статусы ремонта СА with tab5: - st.header("📋 Статусы ремонта СА") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - uploaded_file = st.file_uploader( - "Выберите файл статусов ремонта СА", - type=['xlsx', 'xlsm', 'xls', 'zip'], - key="statuses_repair_ca_upload" - ) - - if uploaded_file is not None: - if st.button("📤 Загрузить файл", key="statuses_repair_ca_upload_btn"): - with st.spinner("Загружаем файл..."): - file_data = uploaded_file.read() - result, status_code = upload_file_to_api("/statuses_repair_ca/upload", file_data, uploaded_file.name) - - if status_code == 200: - st.success("✅ Файл успешно загружен!") - st.json(result) - else: - st.error(f"❌ Ошибка загрузки: {result}") - - # Секция получения данных - st.subheader("📊 Получение данных") - - # Получаем доступные ОГ динамически - available_ogs = get_available_ogs("statuses_repair_ca") - - # Фильтр по ОГ - og_ids = st.multiselect( - "Выберите ОГ (оставьте пустым для всех)", - available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback - key="statuses_repair_ca_og_ids" - ) - - # Предустановленные ключи для извлечения - st.subheader("🔑 Ключи для извлечения данных") - - # Основные ключи - include_basic_keys = st.checkbox("Основные данные", value=True, key="statuses_basic_keys") - include_readiness_keys = st.checkbox("Готовность к КР", value=True, key="statuses_readiness_keys") - include_contract_keys = st.checkbox("Заключение договоров", value=True, key="statuses_contract_keys") - include_supply_keys = st.checkbox("Поставка МТР", value=True, key="statuses_supply_keys") - - # Формируем ключи на основе выбора - keys = [] - if include_basic_keys: - keys.append(["Дата начала ремонта"]) - keys.append(["Отставание / опережение подготовки к КР", "Отставание / опережение"]) - keys.append(["Отставание / опережение подготовки к КР", "Динамика за прошедшую неделю"]) - - if include_readiness_keys: - keys.append(["Готовность к КР", "Факт"]) - - if include_contract_keys: - keys.append(["Заключение договоров на СМР", "Договор", "%"]) - - if include_supply_keys: - keys.append(["Поставка МТР", "На складе, позиций", "%"]) - - # Кнопка получения данных - if st.button("📊 Получить данные", key="statuses_repair_ca_get_data_btn"): - if not keys: - st.warning("⚠️ Выберите хотя бы одну группу ключей для извлечения") - else: - with st.spinner("Получаем данные..."): - request_data = { - "ids": og_ids if og_ids else None, - "keys": keys - } - - result, status_code = make_api_request("/statuses_repair_ca/get_data", request_data) - - if status_code == 200 and result.get("success"): - st.success("✅ Данные успешно получены!") - - data = result.get("data", {}).get("value", []) - if data: - # Отображаем данные в виде таблицы - if isinstance(data, list) and len(data) > 0: - # Преобразуем в DataFrame для лучшего отображения - df_data = [] - for item in data: - row = { - "ID": item.get("id", ""), - "Название": item.get("name", ""), - } - - # Добавляем основные поля - if "Дата начала ремонта" in item: - row["Дата начала ремонта"] = item["Дата начала ремонта"] - - # Добавляем готовность к КР - if "Готовность к КР" in item: - readiness = item["Готовность к КР"] - if isinstance(readiness, dict) and "Факт" in readiness: - row["Готовность к КР (Факт)"] = readiness["Факт"] - - # Добавляем отставание/опережение - if "Отставание / опережение подготовки к КР" in item: - delay = item["Отставание / опережение подготовки к КР"] - if isinstance(delay, dict): - if "Отставание / опережение" in delay: - row["Отставание/опережение"] = delay["Отставание / опережение"] - if "Динамика за прошедшую неделю" in delay: - row["Динамика за неделю"] = delay["Динамика за прошедшую неделю"] - - # Добавляем договоры - if "Заключение договоров на СМР" in item: - contracts = item["Заключение договоров на СМР"] - if isinstance(contracts, dict) and "Договор" in contracts: - contract = contracts["Договор"] - if isinstance(contract, dict) and "%" in contract: - row["Договоры (%)"] = contract["%"] - - # Добавляем поставки МТР - if "Поставка МТР" in item: - supply = item["Поставка МТР"] - if isinstance(supply, dict) and "На складе, позиций" in supply: - warehouse = supply["На складе, позиций"] - if isinstance(warehouse, dict) and "%" in warehouse: - row["МТР на складе (%)"] = warehouse["%"] - - df_data.append(row) - - if df_data: - df = pd.DataFrame(df_data) - st.dataframe(df, use_container_width=True) - else: - st.info("📋 Нет данных для отображения") - else: - st.json(result) - else: - st.info("📋 Нет данных для отображения") - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + render_statuses_repair_ca_tab() # Вкладка 6: Мониторинг ТЭР with tab6: - st.header("⚡ Мониторинг ТЭР (Топливно-энергетических ресурсов)") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - uploaded_file = st.file_uploader( - "Выберите ZIP архив с файлами мониторинга ТЭР", - type=['zip'], - key="monitoring_tar_upload" - ) - - if uploaded_file is not None: - if st.button("📤 Загрузить файл", key="monitoring_tar_upload_btn"): - with st.spinner("Загружаем файл..."): - file_data = uploaded_file.read() - result, status_code = upload_file_to_api("/monitoring_tar/upload", file_data, uploaded_file.name) - - if status_code == 200: - st.success("✅ Файл успешно загружен!") - st.json(result) - else: - st.error(f"❌ Ошибка загрузки: {result}") - - # Секция получения данных - st.subheader("📊 Получение данных") - - # Выбор формата отображения - display_format = st.radio( - "Формат отображения:", - ["JSON", "Таблица"], - key="monitoring_tar_display_format", - horizontal=True - ) - - # Выбор режима данных - mode = st.selectbox( - "Выберите режим данных:", - ["all", "total", "last_day"], - help="total - строки 'Всего' (агрегированные данные), last_day - последние строки данных, all - все данные", - key="monitoring_tar_mode" - ) - - if st.button("📊 Получить данные", key="monitoring_tar_get_data_btn"): - with st.spinner("Получаем данные..."): - # Выбираем эндпоинт в зависимости от режима - if mode == "all": - # Используем полный эндпоинт - result, status_code = make_api_request("/monitoring_tar/get_full_data", {}) - else: - # Используем фильтрованный эндпоинт - request_data = {"mode": mode} - result, status_code = make_api_request("/monitoring_tar/get_data", request_data) - - if status_code == 200 and result.get("success"): - st.success("✅ Данные успешно получены!") - - # Показываем данные - data = result.get("data", {}).get("value", {}) - if data: - st.subheader("📋 Результат:") - - # # Отладочная информация - # st.write(f"🔍 Тип данных: {type(data)}") - # if isinstance(data, str): - # st.write(f"🔍 Длина строки: {len(data)}") - # st.write(f"🔍 Первые 200 символов: {data[:200]}...") - - # Парсим данные, если они пришли как строка - if isinstance(data, str): - try: - import json - data = json.loads(data) - st.write("✅ JSON успешно распарсен") - except json.JSONDecodeError as e: - st.error(f"❌ Ошибка при парсинге JSON данных: {e}") - st.write("Сырые данные:", data) - return - - if display_format == "JSON": - # Отображаем как JSON - st.json(data) - else: - # Отображаем как таблицы - if isinstance(data, dict): - # Показываем данные по установкам - for installation_id, installation_data in data.items(): - with st.expander(f"🏭 {installation_id}"): - if isinstance(installation_data, dict): - # Показываем структуру данных - for data_type, type_data in installation_data.items(): - st.write(f"**{data_type}:**") - if isinstance(type_data, list) and type_data: - df = pd.DataFrame(type_data) - st.dataframe(df) - else: - st.write("Нет данных") - else: - st.write("Нет данных") - else: - st.json(data) - else: - st.info("📋 Нет данных для отображения") - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + render_monitoring_tar_tab() # Вкладка 7: Операционные справки технологических позиций with tab7: - st.header("🏭 Операционные справки технологических позиций") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - - uploaded_file = st.file_uploader( - "Выберите ZIP архив с файлами операционных справок", - type=['zip'], - key="oper_spravka_tech_pos_upload" - ) - - if uploaded_file is not None: - if st.button("📤 Загрузить файл", key="oper_spravka_tech_pos_upload_btn"): - with st.spinner("Загружаем файл..."): - file_data = uploaded_file.read() - result, status_code = upload_file_to_api("/oper_spravka_tech_pos/upload", file_data, uploaded_file.name) - - if status_code == 200: - st.success("✅ Файл успешно загружен!") - st.json(result) - else: - st.error(f"❌ Ошибка загрузки: {result}") - - st.markdown("---") - - # Секция получения данных - st.subheader("📊 Получение данных") - - # Выбор формата отображения - display_format = st.radio( - "Формат отображения:", - ["JSON", "Таблица"], - key="oper_spravka_tech_pos_display_format", - horizontal=True - ) - - # Получаем доступные ОГ динамически - available_ogs = get_available_ogs("oper_spravka_tech_pos") - - # Выбор ОГ - og_id = st.selectbox( - "Выберите ОГ:", - available_ogs if available_ogs else ["SNPZ", "KNPZ", "ANHK", "BASH", "UNH", "NOV"], - key="oper_spravka_tech_pos_og_id" - ) - - if st.button("📊 Получить данные", key="oper_spravka_tech_pos_get_data_btn"): - with st.spinner("Получаем данные..."): - request_data = {"id": og_id} - result, status_code = make_api_request("/oper_spravka_tech_pos/get_data", request_data) - - if status_code == 200 and result.get("success"): - st.success("✅ Данные успешно получены!") - - # Показываем данные - data = result.get("data", []) - - if data and len(data) > 0: - st.subheader("📋 Результат:") - - if display_format == "JSON": - # Отображаем как JSON - st.json(data) - else: - # Отображаем как таблицу - if isinstance(data, list) and data: - df = pd.DataFrame(data) - st.dataframe(df, use_container_width=True) - else: - st.write("Нет данных") - else: - st.info("📋 Нет данных для отображения") - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + render_oper_spravka_tech_pos_tab() # Футер - st.markdown("---") - st.markdown("### 📚 Документация API") - st.markdown(f"Полная документация доступна по адресу: {API_PUBLIC_URL}/docs") - - # Информация о проекте - with st.expander("ℹ️ О проекте"): - st.markdown(""" - **NIN Excel Parsers API** - это веб-сервис для парсинга и обработки Excel-файлов нефтеперерабатывающих заводов. - - **Возможности:** - - 📊 Парсинг сводок ПМ (план и факт) - - 🏭 Парсинг сводок СА - - ⛽ Мониторинг топлива - - ⚡ Мониторинг ТЭР (Топливно-энергетические ресурсы) - - 🔧 Управление ремонтными работами СА - - 📋 Мониторинг статусов ремонта СА - - **Технологии:** - - FastAPI - - Pandas - - MinIO (S3-совместимое хранилище) - - Streamlit (веб-интерфейс) - """) + render_footer() if __name__ == "__main__": main() \ No newline at end of file