diff --git a/.DS_Store b/.DS_Store index e7b720c..b5024b4 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 36b13f1..c9cb45f 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,4 @@ cython_debug/ # PyPI configuration file .pypirc +.DS_Store \ No newline at end of file diff --git a/src/api/medods.py b/src/api/medods.py index 004f0d5..8621e86 100644 --- a/src/api/medods.py +++ b/src/api/medods.py @@ -69,3 +69,21 @@ async def getAllEmployees(host: str, port: int, identity: str, secret: str): offset += limit return employees + + +async def findPatientByPhone( + host: str, port: int, identity: str, secret: str, phone: str +): + bearerToken = generate_token(identity, secret) + headers = {"Authorization": f"Bearer {bearerToken}"} + params = { + "phone": phone, + "limit": 10, + "offset": 0, + } + return await requestGET( + f"http://{host}:{port}/api/v2/clients", + headers=headers, + params=params, + verify_ssl=False, + ) diff --git a/src/api/n3health.py b/src/api/n3health.py index 7ffda1e..7e1562c 100644 --- a/src/api/n3health.py +++ b/src/api/n3health.py @@ -1,4 +1,7 @@ -import config +import asyncio +from datetime import datetime, timedelta +from functools import wraps + from db.schemas import Settings from utils import ( logger, @@ -15,9 +18,48 @@ def getVerifySSL(url: str): return "demo" not in url.lower() -# TODO Проработать лимит в 50 запросов за 15 минут +def rate_limit(max_calls: int, period: int): + """ + Декоратор для ограничения количества вызовов функции + :param max_calls: максимальное количество вызовов + :param period: период в секундах + """ + + def decorator(func): + calls = [] + + @wraps(func) + async def wrapper(*args, **kwargs): + now = datetime.now() + # Удаляем старые вызовы + calls[:] = [ + call_time + for call_time in calls + if now - call_time < timedelta(seconds=period) + ] + + if len(calls) >= max_calls: + oldest_call = calls[0] + wait_time = ( + oldest_call + timedelta(seconds=period) - now + ).total_seconds() + if wait_time > 0: + logger.warning( + f"Превышен лимит запросов. Ожидание {wait_time:.2f} секунд" + ) + await asyncio.sleep(wait_time) + # После ожидания рекурсивно вызываем функцию + return await wrapper(*args, **kwargs) + + calls.append(now) + return await func(*args, **kwargs) + + return wrapper + + return decorator +@rate_limit(max_calls=50, period=900) async def sendForSigning(inData: dict): settings = await Settings.getSettings(False) if not settings.success: @@ -29,9 +71,9 @@ async def sendForSigning(inData: dict): "idLpu": settings.n3healthXIdLpu, }, "practitioner": inData.get("practitioner", {}), - "patients": inData.get("patients", {}), + "patients": [inData.get("patient", {}), *inData.get("recipients", [])], "medDocument": inData.get("medDocument", {}), - "delivery": ["sms"], + "delivery": [inData.get("deliveryType", "sms")], } headers = { @@ -49,7 +91,7 @@ async def sendForSigning(inData: dict): return response -async def resend(trackingID: str, IdPatientMis: str): +async def resend(trackingID: str, IdsPatientMis: list[str], deliveryType: str = "sms"): settings = await Settings.getSettings(False) if not settings.success: logger.error("Настройки не инициализированы") @@ -61,8 +103,8 @@ async def resend(trackingID: str, IdPatientMis: str): } data = { "trackingId": trackingID, - "IdPatientMis": [str(IdPatientMis)], - "delivery": ["sms"], + "IdPatientMis": [str(IdPatientMis) for IdPatientMis in IdsPatientMis], + "delivery": [deliveryType], } response = await requestPOST( f"{settings.n3healthHost}/resend", @@ -108,32 +150,7 @@ async def checkConnection(host: str, authorization: str, xIdLpu: str): ) -async def getAllSigning(): - settings = await Settings.getSettings(False) - if not settings.success: - logger.error("Настройки не инициализированы") - return answer(success=False, message="Настройки не инициализированы") - apiUrl = settings.data.n3healthHost - path = "/signing" - headers = {"Authorization": config.N3_KEY, "X-Id-Lpu": config.N3_ID} - response = await requestGET(apiUrl + path, headers=headers, verify_ssl=False) - logger.info(response) - return response - - -async def getMisIdSigning(idPatientMis: str): - apiUrl = config.N3_URL - path = "/signing" - headers = {"Authorization": config.N3_KEY, "X-Id-Lpu": config.N3_ID} - parameters = {"idPatientMis": idPatientMis} - response = await requestGET( - apiUrl + path, headers=headers, params=parameters, verify_ssl=False - ) - logger.info(response) - return response - - -async def getTrackingIDSigning(trackingID: str, latestStatusCode: int, singingId: int): +async def getTrackingIDSigning(trackingID: str, singingId: int): settings = await Settings.getSettings(False) if not settings.success: logger.error("Настройки не инициализированы") @@ -154,24 +171,25 @@ async def getTrackingIDSigning(trackingID: str, latestStatusCode: int, singingId patients = response.data.get("data", {}).get("patients", []) - status = {"code": 0, "description": ""} - - patientsIds = [] + statuses = {"global": int(response.data.get("data", {}).get("status", 0))} for patient in patients: - patientsIds.append(patient["idPatientMis"]) - if patient.get("status", {}).get("code", 0) > status["code"]: - status = patient.get("status", {}) + idPatientMis = patient.get("idPatientMis") + if idPatientMis not in statuses: + statuses[idPatientMis] = 0 + if patient.get("status", {}).get("code", 0) > statuses[idPatientMis]: + statuses[idPatientMis] = patient.get("status", {}).get("code", 0) - message = "Файлы успешно сохранены" + messages = [] - if latestStatusCode != status["code"]: + if statuses["global"] in [210, 211]: attachments = ( response.data.get("data", {}).get("medDocument", {}).get("attachments", []) ) if len(attachments) > 0: + statuses["storagePath"] = [] for idx, attachment in enumerate(attachments, start=1): content = attachment.get("content") @@ -179,20 +197,18 @@ async def getTrackingIDSigning(trackingID: str, latestStatusCode: int, singingId continue ATTACHMENTS_DIR = Path("src/attachments/import") + result = detect_and_save_attachment( + b64_content=content, + output_dir=ATTACHMENTS_DIR / str(singingId), + filename_prefix=f"document_{idx}", + save_attachments=True, + ) - for patientId in patientsIds: + if not result: + message = f"Файл 'document_{idx}' не сохранены" + logger.error(message) + messages.append(message) + else: + statuses["storagePath"].append(str(result["path"])) - result = detect_and_save_attachment( - b64_content=content, - output_dir=ATTACHMENTS_DIR / patientId / str(singingId), - filename_prefix=f"document_{idx}", - save_attachments=True, - ) - - if not result: - logger.error("Файлы не сохранены") - message = "Файлы не сохранены" - else: - status["storagePath"] = str(result["path"]) - - return answer(data=status, message=message) + return answer(data=statuses, message=", ".join(messages)) diff --git a/src/app/routers/api.py b/src/app/routers/api.py index b8e4675..8225738 100644 --- a/src/app/routers/api.py +++ b/src/app/routers/api.py @@ -1,5 +1,6 @@ from pathlib import Path from fastapi import APIRouter, Depends, HTTPException +from api.medods import findPatientByPhone from api.n3health import resend, revoke, sendForSigning from db.schemas.documents import Document from db.schemas.patients import Patient @@ -37,9 +38,13 @@ async def pre_singing( signature["expiration"] = practitioner.data.get("expired_at")[:10] daysRemainingControl = 0 + deliveryType = None settings = await Settings.getSettings() if settings.success: daysRemainingControl = settings.data.get("expirationAlert", 0) + deliveryData = settings.data.get("deliveryType", {}) + deliveryType = [k for k, v in deliveryData.items() if v] + multipleSending = settings.data.get("multipleSending", False) inProgress = [] complete = [] @@ -75,6 +80,8 @@ async def pre_singing( "inProgress": inProgress, "complete": complete, "daysRemainingControl": daysRemainingControl, + "deliveryType": deliveryType, + "multipleSending": multipleSending, } return exitData @@ -85,8 +92,9 @@ async def singing( ): def correctData(data): data["practitioner"]["userIdLpu"] = str(data["practitioner"]["userIdLpu"]) - for patient in data["patients"]: - patient["idPatientMis"] = str(patient["idPatientMis"]) + data["patient"]["idPatientMis"] = str(data["patient"]["idPatientMis"]) + for recipient in data.get("recipients", []): + recipient["idPatientMis"] = str(recipient["idPatientMis"]) return data logger.info(f"📥 Получен запрос на /singing") @@ -110,57 +118,109 @@ async def singing( return {"status": "ERROR", "message": "Сотрудник не найден"} esiaAuth = practitioner.data.get("esiaAuth", False) userIdLpu = practitioner.data.get("userIdLpu", 0) + body["practitioner"]["snils"] = practitioner.data.get("snils") + if not esiaAuth: + body["practitioner"]["mrProxyNumber"] = practitioner.data.get("attorney") - patients = body.get("patients", []) - if len(patients) == 0: + patient = body.get("patient", None) + if not patient: logger.error("Данные пациента не получены, пациент не найден") return { "status": "ERROR", "message": "Данные пациента не получены, пациент не найден", } + idPatientMis = patient.get("idPatientMis", None) - patientsData = [] + if not idPatientMis: + logger.error("Не указан идентификатор пациента") + return {"status": "ERROR", "message": "Не указан идентификатор пациента"} + patientDB = await Patient.getPatientByIdPatientMis(idPatientMis, isCheck=True) + if not patientDB.success: + patientData = { + "idPatientMis": idPatientMis, + "familyName": patient.get("familyName", "N/A"), + "givenName": patient.get("givenName", "N/A"), + "middleName": patient.get("middleName", "N/A"), + "birthDate": patient.get("birthDate", "N/A"), + "sex": patient.get("sex", "N/A"), + } + newPatient = await Patient.addPatient(**patientData) + if not newPatient.success: + return { + "status": "ERROR", + "message": "Не удалось создать пациента", + } + patientData = newPatient.data.toDict() + else: + patientData = patientDB.data - for patient in patients: - idPatientMis = patient.get("idPatientMis", None) - if not idPatientMis: + recipients = body.pop("recipients", []) + + recipientsData = [] + recipientsIds = [] + + for recipient in recipients: + idRecipientMis = recipient.get("idPatientMis", None) + if not idRecipientMis: logger.error("Не указан идентификатор пациента") return { "status": "ERROR", "message": "Не указан идентификатор пациента", } - patientDB = await Patient.getPatientByIdPatientMis(idPatientMis, isCheck=True) - if not patientDB.success: - patientData = { - "idPatientMis": idPatientMis, - "familyName": patient.get("familyName", "N/A"), - "givenName": patient.get("givenName", "N/A"), - "middleName": patient.get("middleName", "N/A"), - "birthDate": patient.get("birthDate", "N/A"), - "sex": patient.get("sex", "N/A"), + recipientDB = await Patient.getPatientByIdPatientMis( + idRecipientMis, isCheck=True + ) + if not recipientDB.success: + recipientData = { + "idPatientMis": idRecipientMis, + "familyName": recipient.get("surname", "N/A"), + "givenName": recipient.get("name", "N/A"), + "middleName": recipient.get("secondName", "N/A"), + "birthDate": recipient.get("birthDate", "N/A"), + "sex": recipient.get("sex", "N/A"), } - newPatient = await Patient.addPatient(**patientData) - if not newPatient.success: + newRecipient = await Patient.addPatient(**recipientData) + if not newRecipient.success: return { "status": "ERROR", - "message": newPatient.message, + "message": newRecipient.message, } - patientsData.append(newPatient.data.toDict()) + recipientsData.append(newRecipient.data.toDict()) + recipientsIds.append(newRecipient.data.idPatientMis) else: - patientsData.append(patientDB.data) + logger.info(recipientDB.data) + recipientsData.append(recipientDB.data) + recipientsIds.append(recipientDB.data.get("idPatientMis")) + + recipient["documentDto"] = [ + { + "docN": recipient.pop("snils"), + "docS": "", + "documentName": "СНИЛС", + "idDocumentType": 223, + "providerName": "ПФР", + } + ] + recipient["telecom"] = [ + { + "system": "Telephone", + "value": f'+{recipient.pop("phone")}', + } + ] + body["recipients"] = recipients # Логируем информацию о документах logger.info(f"👨‍⚕️ Медработник: {practitioner.data.get("name")}") + logger.info(f"👤 Пациент: {patientData.get('name')}") logger.info( - f"👥 Пациенты ({len(patientsData)}): {', '.join([f'{p.get('name')}' for p in patientsData])}" + f"👥 Получатели ({len(recipientsData)}): {', '.join([f'{p.get('name')}' for p in recipientsData])}" ) logger.info(f"📄 Документов для обработки: {len(docs)}") logger.info(f"🔐 ЕСИА: {esiaAuth}") try: # Конвертируем документы в PDF - conversion_result = convert_docs_to_pdfs(docs, patientsData) - # logger.info(conversion_result) + conversion_result = convert_docs_to_pdfs(docs, idPatientMis) if conversion_result.get("status", "") != "SUCCESS": return { @@ -168,7 +228,9 @@ async def singing( "message": "Не удалось сохранить документы", } - newSinging = await Signing.addSigning(userIdLpu, idPatientMis) + newSinging = await Signing.addSigning( + userIdLpu, idPatientMis, body["deliveryType"], recipientsIds + ) if not newSinging: return { "status": "ERROR", @@ -176,37 +238,41 @@ async def singing( } logger.info(f"🔐 Подписание добавлено: {newSinging.id}") - pdfFiles = conversion_result.get("files", {}) + pdfFiles = conversion_result.get("files", []) body["medDocument"] = { "esiaAuth": esiaAuth, "attachments": [], } body["files"] = [] - for key in pdfFiles: - pdfFile = pdfFiles[key] - for doc in pdfFile: + for doc in pdfFiles: + for idPatient in [idPatientMis, *recipientsIds]: newDocument = await Document.addDocument( - singingId=newSinging.id, idPatientMis=key, **doc + singingId=newSinging.id, idPatientMis=str(idPatient), **doc ) if not newDocument: + logger.error( + f"Не удалось добавить документ для получателя {idPatient}" + ) return { "status": "ERROR", "message": "Не удалось добавить документ", } - logger.info(f"📄 Документ добавлен: {newDocument.id}") - partName = f'file{len(body["medDocument"]["attachments"])}' - body["medDocument"]["attachments"].append( - { - "partName": partName, - } - ) - body["files"].append( - { - "field": partName, - "path": Path(doc["storagePath"]), - "content_type": "application/pdf", - } + logger.debug( + f"📄 Документ добавлен: {newDocument.id} для получателя {idPatient}" ) + partName = f'file{len(body["medDocument"]["attachments"])}' + body["medDocument"]["attachments"].append( + { + "partName": partName, + } + ) + body["files"].append( + { + "field": partName, + "path": Path(doc["storagePath"]), + "content_type": "application/pdf", + } + ) if esiaAuth: partName = f'file{len(body["medDocument"]["attachments"])}' @@ -234,7 +300,7 @@ async def singing( newSinging.trackingId = signingResult.get("trackingId", "") await newSinging.save() - logger.info( + logger.debug( f"🔐 Подписание обновлено: {newSinging.id}. Добавлен Трек-номер подписания" ) @@ -242,10 +308,9 @@ async def singing( logger.info(f"✅ Обработка завершена!") - # Возвращаем ответ response = { "status": "SUCCESS", - "message": f"Документы успешно обработаны. Трек-номер подписания: {newSinging.trackingId}", + "message": "Документы успешно обработаны.", } return response @@ -274,6 +339,16 @@ async def statuses( result = await Signing.getFilteredSingings(userIdLpu, filters) singingsData = result.data + for singingData in singingsData: + documents = singingData.pop("documents", []) + uniqueDocs = [] + uniquePaths = [] + for doc in documents: + if doc.get("storagePath") not in uniquePaths: + uniquePaths.append(doc.get("storagePath")) + uniqueDocs.append(doc) + singingData["documents"] = uniqueDocs + return singingsData @@ -290,7 +365,7 @@ async def documents( logger.error("Не отправлен idPatientMis") return {"status": "ERROR", "message": "Не отправлен idPatientMis"} - idPatientMis = body.get("idPatientMis") + idPatientMis = str(body.get("idPatientMis")) singingData = await Signing.getSigningsByIdPatientMis(idPatientMis) @@ -308,7 +383,38 @@ async def documents( for s in singingData.data: s["practitioner"] = practitioners[s.get("userIdLpu")] - responseData = {"status": "SUCCESS", "data": singingData.data} + for singing in singingData.data: + statusesData = [ + {d.get("idPatientMis"): d.get("patient", {}).get("name")} + for d in singing.get("statuses", []) + if d.get("idPatientMis") + ] + idMatchName = {} + for statusData in statusesData: + for idPatientMisStatus, name in statusData.items(): + if idPatientMisStatus in idMatchName: + continue + idMatchName[idPatientMisStatus] = name + + documents = singing.pop("documents") + uniqueDocs = [] + for doc in documents: + docOwner = doc.get("storagePath").split("/")[-2:-1][0] + isOwn = docOwner == idPatientMis + if str(doc.get("idPatientMis")) == idPatientMis: + if not isOwn: + ownerName = idMatchName[docOwner] + doc["title"] = ( + f"{doc.get('title')} (Пациент: {idMatchName[docOwner]})" + ) + uniqueDocs.append(doc) + singing["documents"] = uniqueDocs + + settings = await Settings.getSettings(False) + + data = {"deliveryType": settings.data.deliveryType, "singings": singingData.data} + + responseData = {"status": "SUCCESS", "data": data} return responseData @@ -335,6 +441,7 @@ async def revoke_post( reqData: dict = Depends(requestDict), ): logger.info(f"📥 Получен запрос на /revoke") + trackingId = reqData.get("body", {}).get("trackingId", None) if not trackingId: return {"status": "ERROR", "message": "Документ не найден"} @@ -359,35 +466,25 @@ async def resend_post( reqData: dict = Depends(requestDict), ): logger.info(f"📥 Получен запрос на /resend") + trackingId = reqData.get("body", {}).get("trackingId", None) idPatientMis = reqData.get("body", {}).get("idPatientMis", None) + deliveryType = reqData.get("body", {}).get("deliveryType", None) errorResponse = {"status": "ERROR", "message": "Повторная отправка не удалось"} - if not trackingId or not idPatientMis: - logger.error("Не отправлен trackingId или idPatientMis") - logger.error(f"trackingId: {trackingId}, idPatientMis: {idPatientMis}") + if not trackingId or not idPatientMis or not deliveryType: + logger.error("Не отправлен trackingId или idPatientMis или deliveryType") + logger.error( + f"trackingId: {trackingId}, idPatientMis: {idPatientMis}, deliveryType: {deliveryType}" + ) return errorResponse - resendResult = await resend(trackingId, idPatientMis) + resendResult = await resend(trackingId, idPatientMis, deliveryType) - if not resendResult or not resendResult.get("status", ""): + if not resendResult or not resendResult.get("success", False): logger.error(resendResult) return errorResponse - newTrackingId = resendResult.get("tracking_id", "") - singingId = reqData.get("body", {}).get("id", "") - - if not newTrackingId or not singingId: - logger.error("Не отправлен trackingId или id") - logger.error(f"trackingId: {newTrackingId}, id: {singingId}") - return errorResponse - - singing = await Signing.getSigningById(singingId) - if not singing.success: - logger.error(singing.message) - return errorResponse - - await singing.data.edit(trackingId=newTrackingId) logger.info("✅ Повторная отправка успешна") return {"status": "SUCCESS", "message": resendResult.get("message", "")} @@ -399,7 +496,70 @@ async def advanced_search_post( ): logger.info(f"📥 Получен запрос на /advanced-search") - result = await Signing.getFilteredSingings( - "", reqData.get("body", {}).get("filters", {}) - ) + filters = reqData.get("body", {}).get("filters", {}) + result = await Signing.getFilteredSingings("", filters) + + for singingData in result.data: + documents = singingData.pop("documents", []) + uniqueDocs = [] + uniquePaths = [] + for doc in documents: + if doc.get("storagePath") not in uniquePaths: + uniquePaths.append(doc.get("storagePath")) + uniqueDocs.append(doc) + singingData["documents"] = uniqueDocs + return {"status": "SUCCESS", "data": result.data} + + +@router.post("/search-recipients", summary="search-recipients") +async def search_recipients_post( + reqData: dict = Depends(requestDict), +): + logger.info(f"📥 Получен запрос на /search-recipients") + phoneNumber = "".join( + filter(str.isdigit, reqData.get("body", {}).get("phone", "")) + )[-10:] + idPatientMis = str(reqData.get("body", {}).get("idPatientMis", "")) + + settings = await Settings.getSettings(False) + if not settings.success: + logger.error(settings.error) + return {"status": "error", "message": settings.error} + settings = settings.data + try: + patientsDB = await findPatientByPhone( + settings.medodsApiHost, + settings.medodsApiPort, + settings.medodsApiIdentity, + settings.medodsApiSecretKey, + f"7{phoneNumber}", + ) + if not patientsDB.success or patientsDB.data.get("totalItems") == 0: + logger.error("Пациент не найден") + return {"status": "error", "data": []} + patients = patientsDB.data.get("data", []) + result = [ + { + "idPatientMis": str(patient.get("id")), + "name": patient.get("name"), + "surname": patient.get("surname"), + "secondName": patient.get("secondName"), + "birthDate": patient.get("birthdate"), + "sex": patient.get("sex"), + "snils": patient.get("snils"), + "phone": patient.get("phone"), + } + for patient in patients + if str(patient.get("id")) != idPatientMis + ] + return { + "status": "ok", + "data": result, + } + except Exception as e: + logger.error(e) + return { + "status": "error", + "data": [], + } diff --git a/src/app/routers/settings.py b/src/app/routers/settings.py index 02700f1..24600a5 100644 --- a/src/app/routers/settings.py +++ b/src/app/routers/settings.py @@ -17,6 +17,7 @@ async def settingsPage(request: Request): @router.get("/get", name="getSettings", summary="Получить настройки") async def getSettings(): settings = await Settings.getSettings() + # logger.info(settings.data) return settings.data @@ -33,6 +34,30 @@ async def updatePassword(reqData: dict = Depends(requestDict)): return response +@router.post( + "/update-sending", name="updateSending", summary="Обновить настройку sending" +) +async def updateSending(reqData: dict = Depends(requestDict)): + logger.info(f"📥 Получен запрос на /update-sending") + response = {"status": "error"} + sendingData = reqData.get("body", {}) + await Settings.updateSettings(**sendingData) + response["status"] = "ok" + return response + + +@router.post( + "/update-delivery", name="updateDelivery", summary="Обновить настройку delivery" +) +async def updateDelivery(reqData: dict = Depends(requestDict)): + logger.info(f"📥 Получен запрос на /update-delivery") + response = {"status": "error"} + deliveryType = reqData.get("body", {}) + await Settings.updateSettings(deliveryType=deliveryType) + response["status"] = "ok" + return response + + @router.post("/test-medods", name="testMedods", summary="Проверить соединение с Медодс") async def testMedods(reqData: dict = Depends(requestDict)): logger.info(f"📥 Получен запрос на /test-medods") diff --git a/src/app/static/js/index.js b/src/app/static/js/index.js index b554438..35a48bf 100644 --- a/src/app/static/js/index.js +++ b/src/app/static/js/index.js @@ -2,7 +2,7 @@ let systemStatus = { medods: { configured: false, details: {} }, n3health: { configured: false, details: {} }, - password: { configured: true } + delivery: { configured: false, types: {} } // Добавлен новый объект для способов доставки }; // Глобальные переменные для статистики сотрудников @@ -44,6 +44,16 @@ async function loadSystemStatus() { hasLpuId: !!data.n3healthXIdLpu }; + // Обновляем статус способов доставки + if (data.deliveryType) { + systemStatus.delivery.types = data.deliveryType; + // Проверяем, что хотя бы один способ активен + systemStatus.delivery.configured = Object.values(data.deliveryType).some(v => v === true); + } else { + systemStatus.delivery.types = { max: false, sms: false, mila: false, goskey: false }; + systemStatus.delivery.configured = false; + } + // Обновляем UI updateStatusUI(); updateReadinessIndicator(); @@ -227,6 +237,50 @@ function updateStatusUI() {
Идентификатор МО: нет
`; } + + // Способы доставки - с обновленными иконками + const deliveryBadge = document.getElementById('deliveryStatusBadge'); + const deliveryDetails = document.getElementById('deliveryDetails'); + const types = systemStatus.delivery.types; + const activeTypes = []; + + if (types.max) activeTypes.push('MAX'); + if (types.sms) activeTypes.push('SMS'); + if (types.mila) activeTypes.push('MILA'); + if (types.goskey) activeTypes.push('GOSKEY'); + + if (systemStatus.delivery.configured && activeTypes.length > 0) { + deliveryBadge.className = 'badge bg-success'; + deliveryBadge.textContent = `Активны (${activeTypes.length})`; + + let detailsHtml = ''; + const allTypes = [ + { name: 'MAX', active: types.max, icon: 'bi-chat-fill', desc: 'Активно' }, + { name: 'SMS', active: types.sms, icon: 'bi-envelope-fill', desc: 'Активно' }, + { name: 'MILA', active: types.mila, icon: 'bi-shield-fill', desc: 'Активно' }, + { name: 'GOSKEY', active: types.goskey, icon: 'bi-key-fill', desc: 'Активно' } + ]; + + allTypes.forEach(type => { + if (type.active) { + detailsHtml += `
${type.name} (${type.desc})
`; + } else { + detailsHtml += `
${type.name} (неактивен)
`; + } + }); + + deliveryDetails.innerHTML = detailsHtml; + } else { + deliveryBadge.className = 'badge bg-danger'; + deliveryBadge.textContent = 'Не настроены'; + + deliveryDetails.innerHTML = ` +
MAX (чат) - неактивен
+
SMS - неактивен
+
MILA - неактивен
+
GOSKEY - неактивен
+ `; + } } // Обновление индикатора готовности и управление кнопкой @@ -236,9 +290,9 @@ function updateReadinessIndicator() { const settingsButtonContainer = document.getElementById('settingsButtonContainer'); let configuredCount = 0; - if (systemStatus.password.configured) configuredCount++; if (systemStatus.medods.configured) configuredCount++; if (systemStatus.n3health.configured) configuredCount++; + if (systemStatus.delivery.configured) configuredCount++; // Заменили password на delivery const readinessPercentage = Math.round((configuredCount / 3) * 100); diff --git a/src/app/static/js/settings.js b/src/app/static/js/settings.js index 8191ca1..a561c2a 100644 --- a/src/app/static/js/settings.js +++ b/src/app/static/js/settings.js @@ -9,12 +9,37 @@ let currentSettings = { host: '', authorization: '', xIdLpu: '' - } + }, + delivery: { // Добавьте эту секцию + max: false, + sms: false, + mila: false, + goskey: false + }, + multipleSending: false }; // Состояния изменений let medodsChanged = false; let n3healthChanged = false; +let deliveryChanged = false; +let sendingChanged = false; + +// функцию обновления кнопок отправки +function updateSendingButtons() { + const saveBtn = document.getElementById('saveSendingBtn'); + const cancelBtn = document.getElementById('cancelSendingBtn'); + + saveBtn.disabled = !sendingChanged; + cancelBtn.disabled = !sendingChanged; +} + +// функцию сброса формы отправки +function resetSendingForm() { + document.getElementById('multipleSending').checked = currentSettings.multipleSending; + sendingChanged = false; + updateSendingButtons(); +} // Утилиты function showAlert(message, type = 'success') { @@ -42,6 +67,46 @@ function showAlert(message, type = 'success') { } } +// Добавьте функцию проверки выбора способов доставки: +function isDeliveryValid() { + const checkboxes = document.querySelectorAll('.delivery-checkbox'); + let checkedCount = 0; + checkboxes.forEach(cb => { + if (cb.checked) checkedCount++; + }); + + const validationMessage = document.getElementById('deliveryValidationMessage'); + if (checkedCount === 0) { + validationMessage.style.display = 'block'; + return false; + } else { + validationMessage.style.display = 'none'; + return true; + } +} + +// Добавьте функцию обновления кнопок доставки: +function updateDeliveryButtons() { + const isValid = isDeliveryValid(); + const saveBtn = document.getElementById('saveDeliveryBtn'); + const cancelBtn = document.getElementById('cancelDeliveryBtn'); + + saveBtn.disabled = !isValid || !deliveryChanged; + cancelBtn.disabled = !deliveryChanged; +} + +// Добавьте функцию сброса формы доставки: +function resetDeliveryForm() { + document.getElementById('deliveryMax').checked = currentSettings.delivery.max; + document.getElementById('deliverySms').checked = currentSettings.delivery.sms; + document.getElementById('deliveryMila').checked = currentSettings.delivery.mila; + document.getElementById('deliveryGoskey').checked = currentSettings.delivery.goskey; + + deliveryChanged = false; + isDeliveryValid(); // Обновляем сообщение валидации + updateDeliveryButtons(); +} + // Проверка заполненности формы Медодс function isMedodsFormValid() { const host = document.getElementById('medodsHost').value; @@ -94,6 +159,30 @@ function updateStatusIndicators() { n3healthStatus.className = 'badge bg-danger'; n3healthStatus.textContent = 'Требуется настройка'; } + + // Способы доставки - обновляем статус + const deliveryStatus = document.getElementById('deliveryStatus'); + const hasDeliveryConfig = Object.values(currentSettings.delivery).some(v => v === true); + + if (hasDeliveryConfig) { + deliveryStatus.className = 'badge bg-success'; + // Показываем количество активных способов + const activeCount = Object.values(currentSettings.delivery).filter(v => v).length; + deliveryStatus.textContent = `${activeCount} активн${activeCount === 1 ? 'ый' : 'ых'}`; + } else { + deliveryStatus.className = 'badge bg-danger'; + deliveryStatus.textContent = 'Не настроен'; + } + + // Настройки отправки - НОВЫЙ БЛОК + const sendingStatus = document.getElementById('sendingStatus'); + if (currentSettings.multipleSending) { + sendingStatus.className = 'badge bg-success'; + sendingStatus.textContent = 'Включено'; + } else { + sendingStatus.className = 'badge bg-secondary'; + sendingStatus.textContent = 'Отключено'; + } } // Обновление кнопок Медодс @@ -219,6 +308,21 @@ async function loadCurrentSettings() { document.getElementById('n3healthXIdLpu').value = data.n3healthXIdLpu; } + // Загружаем настройки доставки + if (data.deliveryType) { + currentSettings.delivery = { ...currentSettings.delivery, ...data.deliveryType }; + document.getElementById('deliveryMax').checked = data.deliveryType.max || false; + document.getElementById('deliverySms').checked = data.deliveryType.sms || false; + document.getElementById('deliveryMila').checked = data.deliveryType.mila || false; + document.getElementById('deliveryGoskey').checked = data.deliveryType.goskey || false; + } + + // Загружаем настройку множественной отправки - НОВЫЙ БЛОК + if (data.multipleSending !== undefined) { + currentSettings.multipleSending = data.multipleSending; + document.getElementById('multipleSending').checked = data.multipleSending; + } + updateStatusIndicators(); } } catch (error) { @@ -264,6 +368,22 @@ function setupChangeListeners() { updateN3HealthButtons(); }); }); + + // Способы доставки + const deliveryCheckboxes = document.querySelectorAll('.delivery-checkbox'); + deliveryCheckboxes.forEach(checkbox => { + checkbox.addEventListener('change', () => { + deliveryChanged = true; + isDeliveryValid(); + updateDeliveryButtons(); + }); + }); + + // Настройки отправки - НОВЫЙ СЛУШАТЕЛЬ + document.getElementById('multipleSending').addEventListener('change', () => { + sendingChanged = true; + updateSendingButtons(); + }); } // Инициализация @@ -532,4 +652,102 @@ document.addEventListener('DOMContentLoaded', function () { // Обработчик отмены изменений N3Health document.getElementById('cancelN3HealthBtn').addEventListener('click', resetN3HealthForm); + + // Обработчик сохранения настроек доставки + document.getElementById('deliveryForm').addEventListener('submit', async function (e) { + e.preventDefault(); + + if (!isDeliveryValid()) { + showAlert('Должен быть выбран хотя бы один способ доставки', 'danger'); + return; + } + + const deliveryData = { + max: document.getElementById('deliveryMax').checked, + sms: document.getElementById('deliverySms').checked, + mila: document.getElementById('deliveryMila').checked, + goskey: document.getElementById('deliveryGoskey').checked + }; + + try { + const response = await fetch('/settings/update-delivery', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(deliveryData) + }); + + const data = await response.json(); + + if (response.ok && data.status === "ok") { + showAlert('Настройки способов доставки успешно сохранены', 'success'); + + // Обновляем текущие настройки + currentSettings.delivery = { ...deliveryData }; + + // Сбрасываем флаги изменений + deliveryChanged = false; + + // Обновляем кнопки и статусы + updateDeliveryButtons(); + updateStatusIndicators(); + } else { + showAlert(data.error || 'Ошибка при сохранении', 'danger'); + } + } catch (error) { + console.error('Ошибка:', error); + showAlert('Ошибка при сохранении', 'danger'); + } + }); + + // Обработчик отмены изменений доставки + document.getElementById('cancelDeliveryBtn').addEventListener('click', resetDeliveryForm); + + // Обработчик сохранения настроек отправки - НОВЫЙ + document.getElementById('sendingForm').addEventListener('submit', async function (e) { + e.preventDefault(); + + const multipleSending = document.getElementById('multipleSending').checked; + + try { + const response = await fetch('/settings/update-sending', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + multipleSending: multipleSending + }) + }); + + const data = await response.json(); + + if (response.ok && data.status === "ok") { + showAlert('Настройки отправки успешно сохранены', 'success'); + + // Обновляем текущие настройки + currentSettings.multipleSending = multipleSending; + + // Сбрасываем флаги изменений + sendingChanged = false; + + // Обновляем кнопки и статусы + updateSendingButtons(); + updateStatusIndicators(); + } else { + showAlert(data.error || 'Ошибка при сохранении', 'danger'); + } + } catch (error) { + console.error('Ошибка:', error); + showAlert('Ошибка при сохранении', 'danger'); + } + }); + + // Обработчик отмены изменений отправки - НОВЫЙ + document.getElementById('cancelSendingBtn').addEventListener('click', resetSendingForm); + + // Инициализация состояния кнопок доставки + updateDeliveryButtons(); + updateSendingButtons(); }); \ No newline at end of file diff --git a/src/app/static/js/staff.js b/src/app/static/js/staff.js index d8fa4f6..006c497 100644 --- a/src/app/static/js/staff.js +++ b/src/app/static/js/staff.js @@ -368,11 +368,17 @@ function updateStaffTable() { } else { // Экранируем специальные символы в attorney const attorney = (practitioner.attorney || '').replace(/"/g, '"').replace(/'/g, '''); - const eyeIcon = ``; - signatureTypeHtml = `МЧД${eyeIcon}`; + const snils = (practitioner.snils || '').replace(/"/g, '"').replace(/'/g, '''); + const snilsIcon = ``; + signatureTypeHtml = `МЧД${eyeIcon} + СНИЛС${snilsIcon}`; } // Форматируем дату и проверяем срок @@ -630,6 +636,7 @@ function editPractitioner(userIdLpu, isEsiaAuth) { // Редактирование УКЭП document.getElementById('ukepPractitionerId').value = userIdLpu; document.getElementById('ukepFile').required = false; + document.getElementById('ukepSnilsNumber').value = practitioner.snils; // Очищаем data-атрибуты модального окна const modal = document.getElementById('editUKEPModal'); @@ -650,6 +657,7 @@ function editPractitioner(userIdLpu, isEsiaAuth) { // Редактирование МЧД document.getElementById('mchdPractitionerId').value = userIdLpu; document.getElementById('mchdNumber').value = practitioner.attorney || ''; + document.getElementById('mchdSnilsNumber').value = practitioner.snils; // Очищаем data-атрибуты модального окна const modal = document.getElementById('editMCHDModal'); @@ -779,6 +787,7 @@ document.addEventListener('DOMContentLoaded', function () { const userIdLpu = document.getElementById('mchdPractitionerId').value; const mchdNumber = document.getElementById('mchdNumber').value.trim(); const expiryDate = document.getElementById('mchdExpiryDate').value; + const snils = document.getElementById('mchdSnilsNumber').value.replace(/\D/g, ''); if (!mchdNumber || !expiryDate) { showAlert('Заполните все обязательные поля', 'warning'); @@ -813,6 +822,7 @@ document.addEventListener('DOMContentLoaded', function () { userIdLpu: userIdLpu, esiaAuth: false, attorney: mchdNumber, + snils: snils, expired_at: expiryDate }; @@ -861,6 +871,7 @@ document.addEventListener('DOMContentLoaded', function () { requestBody = { userIdLpu: userIdLpu, attorney: mchdNumber, + snils: snils, expired_at: expiryDate }; @@ -903,6 +914,7 @@ document.addEventListener('DOMContentLoaded', function () { const userIdLpu = document.getElementById('ukepPractitionerId').value; const ukepFile = document.getElementById('ukepFile').files[0]; const expiryDate = document.getElementById('ukepExpiryDate').value; + const snils = document.getElementById('mchdSnilsNumber').value.replace(/\D/g, ''); if (!expiryDate) { showAlert('Заполните дату окончания действия', 'warning'); @@ -942,6 +954,7 @@ document.addEventListener('DOMContentLoaded', function () { userIdLpu: userIdLpu, esiaAuth: true, attorney: '', + snils: snils, expired_at: expiryDate }; @@ -991,6 +1004,7 @@ document.addEventListener('DOMContentLoaded', function () { const updateData = { userIdLpu: userIdLpu, + snils: snils, expired_at: expiryDate }; diff --git a/src/app/templates/index.html b/src/app/templates/index.html index 5dc88c7..6cdfdc1 100644 --- a/src/app/templates/index.html +++ b/src/app/templates/index.html @@ -18,21 +18,21 @@
- +
- +
-
Пароль администратора
- Настроен +
Каналы доставки
+ Не настроены
-

- Пароль администратора установлен и защищает доступ к настройкам системы. -

+
+ +
diff --git a/src/app/templates/settings/index.html b/src/app/templates/settings/index.html index dbda1d2..8e40de4 100644 --- a/src/app/templates/settings/index.html +++ b/src/app/templates/settings/index.html @@ -57,6 +57,120 @@
+ +
+

+ +

+
+
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ +
+ + +
+
+
+
+
+ + +
+

+ +

+
+
+
+
+
+ + +
+
+ Включение и отключение возможности отправлять на подписание одновременно нескольким + получателям. +
+
+ +
+ + +
+
+
+
+
+

diff --git a/src/app/templates/staff/index.html b/src/app/templates/staff/index.html index 70aff82..d42b5f5 100644 --- a/src/app/templates/staff/index.html +++ b/src/app/templates/staff/index.html @@ -164,6 +164,10 @@

+
+ + +
+
+ + +