release 1.1
This commit is contained in:
@@ -174,3 +174,4 @@ cython_debug/
|
|||||||
# PyPI configuration file
|
# PyPI configuration file
|
||||||
.pypirc
|
.pypirc
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
@@ -69,3 +69,21 @@ async def getAllEmployees(host: str, port: int, identity: str, secret: str):
|
|||||||
offset += limit
|
offset += limit
|
||||||
|
|
||||||
return employees
|
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,
|
||||||
|
)
|
||||||
|
|||||||
+65
-49
@@ -1,4 +1,7 @@
|
|||||||
import config
|
import asyncio
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
from db.schemas import Settings
|
from db.schemas import Settings
|
||||||
from utils import (
|
from utils import (
|
||||||
logger,
|
logger,
|
||||||
@@ -15,9 +18,48 @@ def getVerifySSL(url: str):
|
|||||||
return "demo" not in url.lower()
|
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):
|
async def sendForSigning(inData: dict):
|
||||||
settings = await Settings.getSettings(False)
|
settings = await Settings.getSettings(False)
|
||||||
if not settings.success:
|
if not settings.success:
|
||||||
@@ -29,9 +71,9 @@ async def sendForSigning(inData: dict):
|
|||||||
"idLpu": settings.n3healthXIdLpu,
|
"idLpu": settings.n3healthXIdLpu,
|
||||||
},
|
},
|
||||||
"practitioner": inData.get("practitioner", {}),
|
"practitioner": inData.get("practitioner", {}),
|
||||||
"patients": inData.get("patients", {}),
|
"patients": [inData.get("patient", {}), *inData.get("recipients", [])],
|
||||||
"medDocument": inData.get("medDocument", {}),
|
"medDocument": inData.get("medDocument", {}),
|
||||||
"delivery": ["sms"],
|
"delivery": [inData.get("deliveryType", "sms")],
|
||||||
}
|
}
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
@@ -49,7 +91,7 @@ async def sendForSigning(inData: dict):
|
|||||||
return response
|
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)
|
settings = await Settings.getSettings(False)
|
||||||
if not settings.success:
|
if not settings.success:
|
||||||
logger.error("Настройки не инициализированы")
|
logger.error("Настройки не инициализированы")
|
||||||
@@ -61,8 +103,8 @@ async def resend(trackingID: str, IdPatientMis: str):
|
|||||||
}
|
}
|
||||||
data = {
|
data = {
|
||||||
"trackingId": trackingID,
|
"trackingId": trackingID,
|
||||||
"IdPatientMis": [str(IdPatientMis)],
|
"IdPatientMis": [str(IdPatientMis) for IdPatientMis in IdsPatientMis],
|
||||||
"delivery": ["sms"],
|
"delivery": [deliveryType],
|
||||||
}
|
}
|
||||||
response = await requestPOST(
|
response = await requestPOST(
|
||||||
f"{settings.n3healthHost}/resend",
|
f"{settings.n3healthHost}/resend",
|
||||||
@@ -108,32 +150,7 @@ async def checkConnection(host: str, authorization: str, xIdLpu: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def getAllSigning():
|
async def getTrackingIDSigning(trackingID: str, singingId: int):
|
||||||
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):
|
|
||||||
settings = await Settings.getSettings(False)
|
settings = await Settings.getSettings(False)
|
||||||
if not settings.success:
|
if not settings.success:
|
||||||
logger.error("Настройки не инициализированы")
|
logger.error("Настройки не инициализированы")
|
||||||
@@ -154,24 +171,25 @@ async def getTrackingIDSigning(trackingID: str, latestStatusCode: int, singingId
|
|||||||
|
|
||||||
patients = response.data.get("data", {}).get("patients", [])
|
patients = response.data.get("data", {}).get("patients", [])
|
||||||
|
|
||||||
status = {"code": 0, "description": ""}
|
statuses = {"global": int(response.data.get("data", {}).get("status", 0))}
|
||||||
|
|
||||||
patientsIds = []
|
|
||||||
|
|
||||||
for patient in patients:
|
for patient in patients:
|
||||||
patientsIds.append(patient["idPatientMis"])
|
idPatientMis = patient.get("idPatientMis")
|
||||||
if patient.get("status", {}).get("code", 0) > status["code"]:
|
if idPatientMis not in statuses:
|
||||||
status = patient.get("status", {})
|
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 = (
|
attachments = (
|
||||||
response.data.get("data", {}).get("medDocument", {}).get("attachments", [])
|
response.data.get("data", {}).get("medDocument", {}).get("attachments", [])
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(attachments) > 0:
|
if len(attachments) > 0:
|
||||||
|
statuses["storagePath"] = []
|
||||||
|
|
||||||
for idx, attachment in enumerate(attachments, start=1):
|
for idx, attachment in enumerate(attachments, start=1):
|
||||||
content = attachment.get("content")
|
content = attachment.get("content")
|
||||||
@@ -179,20 +197,18 @@ async def getTrackingIDSigning(trackingID: str, latestStatusCode: int, singingId
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
ATTACHMENTS_DIR = Path("src/attachments/import")
|
ATTACHMENTS_DIR = Path("src/attachments/import")
|
||||||
|
|
||||||
for patientId in patientsIds:
|
|
||||||
|
|
||||||
result = detect_and_save_attachment(
|
result = detect_and_save_attachment(
|
||||||
b64_content=content,
|
b64_content=content,
|
||||||
output_dir=ATTACHMENTS_DIR / patientId / str(singingId),
|
output_dir=ATTACHMENTS_DIR / str(singingId),
|
||||||
filename_prefix=f"document_{idx}",
|
filename_prefix=f"document_{idx}",
|
||||||
save_attachments=True,
|
save_attachments=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
logger.error("Файлы не сохранены")
|
message = f"Файл 'document_{idx}' не сохранены"
|
||||||
message = "Файлы не сохранены"
|
logger.error(message)
|
||||||
|
messages.append(message)
|
||||||
else:
|
else:
|
||||||
status["storagePath"] = str(result["path"])
|
statuses["storagePath"].append(str(result["path"]))
|
||||||
|
|
||||||
return answer(data=status, message=message)
|
return answer(data=statuses, message=", ".join(messages))
|
||||||
|
|||||||
+212
-52
@@ -1,5 +1,6 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from api.medods import findPatientByPhone
|
||||||
from api.n3health import resend, revoke, sendForSigning
|
from api.n3health import resend, revoke, sendForSigning
|
||||||
from db.schemas.documents import Document
|
from db.schemas.documents import Document
|
||||||
from db.schemas.patients import Patient
|
from db.schemas.patients import Patient
|
||||||
@@ -37,9 +38,13 @@ async def pre_singing(
|
|||||||
signature["expiration"] = practitioner.data.get("expired_at")[:10]
|
signature["expiration"] = practitioner.data.get("expired_at")[:10]
|
||||||
|
|
||||||
daysRemainingControl = 0
|
daysRemainingControl = 0
|
||||||
|
deliveryType = None
|
||||||
settings = await Settings.getSettings()
|
settings = await Settings.getSettings()
|
||||||
if settings.success:
|
if settings.success:
|
||||||
daysRemainingControl = settings.data.get("expirationAlert", 0)
|
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 = []
|
inProgress = []
|
||||||
complete = []
|
complete = []
|
||||||
@@ -75,6 +80,8 @@ async def pre_singing(
|
|||||||
"inProgress": inProgress,
|
"inProgress": inProgress,
|
||||||
"complete": complete,
|
"complete": complete,
|
||||||
"daysRemainingControl": daysRemainingControl,
|
"daysRemainingControl": daysRemainingControl,
|
||||||
|
"deliveryType": deliveryType,
|
||||||
|
"multipleSending": multipleSending,
|
||||||
}
|
}
|
||||||
return exitData
|
return exitData
|
||||||
|
|
||||||
@@ -85,8 +92,9 @@ async def singing(
|
|||||||
):
|
):
|
||||||
def correctData(data):
|
def correctData(data):
|
||||||
data["practitioner"]["userIdLpu"] = str(data["practitioner"]["userIdLpu"])
|
data["practitioner"]["userIdLpu"] = str(data["practitioner"]["userIdLpu"])
|
||||||
for patient in data["patients"]:
|
data["patient"]["idPatientMis"] = str(data["patient"]["idPatientMis"])
|
||||||
patient["idPatientMis"] = str(patient["idPatientMis"])
|
for recipient in data.get("recipients", []):
|
||||||
|
recipient["idPatientMis"] = str(recipient["idPatientMis"])
|
||||||
return data
|
return data
|
||||||
|
|
||||||
logger.info(f"📥 Получен запрос на /singing")
|
logger.info(f"📥 Получен запрос на /singing")
|
||||||
@@ -110,25 +118,22 @@ async def singing(
|
|||||||
return {"status": "ERROR", "message": "Сотрудник не найден"}
|
return {"status": "ERROR", "message": "Сотрудник не найден"}
|
||||||
esiaAuth = practitioner.data.get("esiaAuth", False)
|
esiaAuth = practitioner.data.get("esiaAuth", False)
|
||||||
userIdLpu = practitioner.data.get("userIdLpu", 0)
|
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", [])
|
patient = body.get("patient", None)
|
||||||
if len(patients) == 0:
|
if not patient:
|
||||||
logger.error("Данные пациента не получены, пациент не найден")
|
logger.error("Данные пациента не получены, пациент не найден")
|
||||||
return {
|
return {
|
||||||
"status": "ERROR",
|
"status": "ERROR",
|
||||||
"message": "Данные пациента не получены, пациент не найден",
|
"message": "Данные пациента не получены, пациент не найден",
|
||||||
}
|
}
|
||||||
|
|
||||||
patientsData = []
|
|
||||||
|
|
||||||
for patient in patients:
|
|
||||||
idPatientMis = patient.get("idPatientMis", None)
|
idPatientMis = patient.get("idPatientMis", None)
|
||||||
|
|
||||||
if not idPatientMis:
|
if not idPatientMis:
|
||||||
logger.error("Не указан идентификатор пациента")
|
logger.error("Не указан идентификатор пациента")
|
||||||
return {
|
return {"status": "ERROR", "message": "Не указан идентификатор пациента"}
|
||||||
"status": "ERROR",
|
|
||||||
"message": "Не указан идентификатор пациента",
|
|
||||||
}
|
|
||||||
patientDB = await Patient.getPatientByIdPatientMis(idPatientMis, isCheck=True)
|
patientDB = await Patient.getPatientByIdPatientMis(idPatientMis, isCheck=True)
|
||||||
if not patientDB.success:
|
if not patientDB.success:
|
||||||
patientData = {
|
patientData = {
|
||||||
@@ -143,24 +148,79 @@ async def singing(
|
|||||||
if not newPatient.success:
|
if not newPatient.success:
|
||||||
return {
|
return {
|
||||||
"status": "ERROR",
|
"status": "ERROR",
|
||||||
"message": newPatient.message,
|
"message": "Не удалось создать пациента",
|
||||||
}
|
}
|
||||||
patientsData.append(newPatient.data.toDict())
|
patientData = newPatient.data.toDict()
|
||||||
else:
|
else:
|
||||||
patientsData.append(patientDB.data)
|
patientData = patientDB.data
|
||||||
|
|
||||||
|
recipients = body.pop("recipients", [])
|
||||||
|
|
||||||
|
recipientsData = []
|
||||||
|
recipientsIds = []
|
||||||
|
|
||||||
|
for recipient in recipients:
|
||||||
|
idRecipientMis = recipient.get("idPatientMis", None)
|
||||||
|
if not idRecipientMis:
|
||||||
|
logger.error("Не указан идентификатор пациента")
|
||||||
|
return {
|
||||||
|
"status": "ERROR",
|
||||||
|
"message": "Не указан идентификатор пациента",
|
||||||
|
}
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
newRecipient = await Patient.addPatient(**recipientData)
|
||||||
|
if not newRecipient.success:
|
||||||
|
return {
|
||||||
|
"status": "ERROR",
|
||||||
|
"message": newRecipient.message,
|
||||||
|
}
|
||||||
|
recipientsData.append(newRecipient.data.toDict())
|
||||||
|
recipientsIds.append(newRecipient.data.idPatientMis)
|
||||||
|
else:
|
||||||
|
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"👨⚕️ Медработник: {practitioner.data.get("name")}")
|
||||||
|
logger.info(f"👤 Пациент: {patientData.get('name')}")
|
||||||
logger.info(
|
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"📄 Документов для обработки: {len(docs)}")
|
||||||
logger.info(f"🔐 ЕСИА: {esiaAuth}")
|
logger.info(f"🔐 ЕСИА: {esiaAuth}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Конвертируем документы в PDF
|
# Конвертируем документы в PDF
|
||||||
conversion_result = convert_docs_to_pdfs(docs, patientsData)
|
conversion_result = convert_docs_to_pdfs(docs, idPatientMis)
|
||||||
# logger.info(conversion_result)
|
|
||||||
|
|
||||||
if conversion_result.get("status", "") != "SUCCESS":
|
if conversion_result.get("status", "") != "SUCCESS":
|
||||||
return {
|
return {
|
||||||
@@ -168,7 +228,9 @@ async def singing(
|
|||||||
"message": "Не удалось сохранить документы",
|
"message": "Не удалось сохранить документы",
|
||||||
}
|
}
|
||||||
|
|
||||||
newSinging = await Signing.addSigning(userIdLpu, idPatientMis)
|
newSinging = await Signing.addSigning(
|
||||||
|
userIdLpu, idPatientMis, body["deliveryType"], recipientsIds
|
||||||
|
)
|
||||||
if not newSinging:
|
if not newSinging:
|
||||||
return {
|
return {
|
||||||
"status": "ERROR",
|
"status": "ERROR",
|
||||||
@@ -176,24 +238,28 @@ async def singing(
|
|||||||
}
|
}
|
||||||
logger.info(f"🔐 Подписание добавлено: {newSinging.id}")
|
logger.info(f"🔐 Подписание добавлено: {newSinging.id}")
|
||||||
|
|
||||||
pdfFiles = conversion_result.get("files", {})
|
pdfFiles = conversion_result.get("files", [])
|
||||||
body["medDocument"] = {
|
body["medDocument"] = {
|
||||||
"esiaAuth": esiaAuth,
|
"esiaAuth": esiaAuth,
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
}
|
}
|
||||||
body["files"] = []
|
body["files"] = []
|
||||||
for key in pdfFiles:
|
for doc in pdfFiles:
|
||||||
pdfFile = pdfFiles[key]
|
for idPatient in [idPatientMis, *recipientsIds]:
|
||||||
for doc in pdfFile:
|
|
||||||
newDocument = await Document.addDocument(
|
newDocument = await Document.addDocument(
|
||||||
singingId=newSinging.id, idPatientMis=key, **doc
|
singingId=newSinging.id, idPatientMis=str(idPatient), **doc
|
||||||
)
|
)
|
||||||
if not newDocument:
|
if not newDocument:
|
||||||
|
logger.error(
|
||||||
|
f"Не удалось добавить документ для получателя {idPatient}"
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"status": "ERROR",
|
"status": "ERROR",
|
||||||
"message": "Не удалось добавить документ",
|
"message": "Не удалось добавить документ",
|
||||||
}
|
}
|
||||||
logger.info(f"📄 Документ добавлен: {newDocument.id}")
|
logger.debug(
|
||||||
|
f"📄 Документ добавлен: {newDocument.id} для получателя {idPatient}"
|
||||||
|
)
|
||||||
partName = f'file{len(body["medDocument"]["attachments"])}'
|
partName = f'file{len(body["medDocument"]["attachments"])}'
|
||||||
body["medDocument"]["attachments"].append(
|
body["medDocument"]["attachments"].append(
|
||||||
{
|
{
|
||||||
@@ -234,7 +300,7 @@ async def singing(
|
|||||||
|
|
||||||
newSinging.trackingId = signingResult.get("trackingId", "")
|
newSinging.trackingId = signingResult.get("trackingId", "")
|
||||||
await newSinging.save()
|
await newSinging.save()
|
||||||
logger.info(
|
logger.debug(
|
||||||
f"🔐 Подписание обновлено: {newSinging.id}. Добавлен Трек-номер подписания"
|
f"🔐 Подписание обновлено: {newSinging.id}. Добавлен Трек-номер подписания"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -242,10 +308,9 @@ async def singing(
|
|||||||
|
|
||||||
logger.info(f"✅ Обработка завершена!")
|
logger.info(f"✅ Обработка завершена!")
|
||||||
|
|
||||||
# Возвращаем ответ
|
|
||||||
response = {
|
response = {
|
||||||
"status": "SUCCESS",
|
"status": "SUCCESS",
|
||||||
"message": f"Документы успешно обработаны. Трек-номер подписания: {newSinging.trackingId}",
|
"message": "Документы успешно обработаны.",
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -274,6 +339,16 @@ async def statuses(
|
|||||||
result = await Signing.getFilteredSingings(userIdLpu, filters)
|
result = await Signing.getFilteredSingings(userIdLpu, filters)
|
||||||
singingsData = result.data
|
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
|
return singingsData
|
||||||
|
|
||||||
|
|
||||||
@@ -290,7 +365,7 @@ async def documents(
|
|||||||
logger.error("Не отправлен idPatientMis")
|
logger.error("Не отправлен idPatientMis")
|
||||||
return {"status": "ERROR", "message": "Не отправлен idPatientMis"}
|
return {"status": "ERROR", "message": "Не отправлен idPatientMis"}
|
||||||
|
|
||||||
idPatientMis = body.get("idPatientMis")
|
idPatientMis = str(body.get("idPatientMis"))
|
||||||
|
|
||||||
singingData = await Signing.getSigningsByIdPatientMis(idPatientMis)
|
singingData = await Signing.getSigningsByIdPatientMis(idPatientMis)
|
||||||
|
|
||||||
@@ -308,7 +383,38 @@ async def documents(
|
|||||||
for s in singingData.data:
|
for s in singingData.data:
|
||||||
s["practitioner"] = practitioners[s.get("userIdLpu")]
|
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
|
return responseData
|
||||||
|
|
||||||
@@ -335,6 +441,7 @@ async def revoke_post(
|
|||||||
reqData: dict = Depends(requestDict),
|
reqData: dict = Depends(requestDict),
|
||||||
):
|
):
|
||||||
logger.info(f"📥 Получен запрос на /revoke")
|
logger.info(f"📥 Получен запрос на /revoke")
|
||||||
|
|
||||||
trackingId = reqData.get("body", {}).get("trackingId", None)
|
trackingId = reqData.get("body", {}).get("trackingId", None)
|
||||||
if not trackingId:
|
if not trackingId:
|
||||||
return {"status": "ERROR", "message": "Документ не найден"}
|
return {"status": "ERROR", "message": "Документ не найден"}
|
||||||
@@ -359,35 +466,25 @@ async def resend_post(
|
|||||||
reqData: dict = Depends(requestDict),
|
reqData: dict = Depends(requestDict),
|
||||||
):
|
):
|
||||||
logger.info(f"📥 Получен запрос на /resend")
|
logger.info(f"📥 Получен запрос на /resend")
|
||||||
|
|
||||||
trackingId = reqData.get("body", {}).get("trackingId", None)
|
trackingId = reqData.get("body", {}).get("trackingId", None)
|
||||||
idPatientMis = reqData.get("body", {}).get("idPatientMis", None)
|
idPatientMis = reqData.get("body", {}).get("idPatientMis", None)
|
||||||
|
deliveryType = reqData.get("body", {}).get("deliveryType", None)
|
||||||
|
|
||||||
errorResponse = {"status": "ERROR", "message": "Повторная отправка не удалось"}
|
errorResponse = {"status": "ERROR", "message": "Повторная отправка не удалось"}
|
||||||
if not trackingId or not idPatientMis:
|
if not trackingId or not idPatientMis or not deliveryType:
|
||||||
logger.error("Не отправлен trackingId или idPatientMis")
|
logger.error("Не отправлен trackingId или idPatientMis или deliveryType")
|
||||||
logger.error(f"trackingId: {trackingId}, idPatientMis: {idPatientMis}")
|
logger.error(
|
||||||
|
f"trackingId: {trackingId}, idPatientMis: {idPatientMis}, deliveryType: {deliveryType}"
|
||||||
|
)
|
||||||
return errorResponse
|
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)
|
logger.error(resendResult)
|
||||||
return errorResponse
|
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("✅ Повторная отправка успешна")
|
logger.info("✅ Повторная отправка успешна")
|
||||||
|
|
||||||
return {"status": "SUCCESS", "message": resendResult.get("message", "")}
|
return {"status": "SUCCESS", "message": resendResult.get("message", "")}
|
||||||
@@ -399,7 +496,70 @@ async def advanced_search_post(
|
|||||||
):
|
):
|
||||||
logger.info(f"📥 Получен запрос на /advanced-search")
|
logger.info(f"📥 Получен запрос на /advanced-search")
|
||||||
|
|
||||||
result = await Signing.getFilteredSingings(
|
filters = reqData.get("body", {}).get("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}
|
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": [],
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ async def settingsPage(request: Request):
|
|||||||
@router.get("/get", name="getSettings", summary="Получить настройки")
|
@router.get("/get", name="getSettings", summary="Получить настройки")
|
||||||
async def getSettings():
|
async def getSettings():
|
||||||
settings = await Settings.getSettings()
|
settings = await Settings.getSettings()
|
||||||
|
# logger.info(settings.data)
|
||||||
return settings.data
|
return settings.data
|
||||||
|
|
||||||
|
|
||||||
@@ -33,6 +34,30 @@ async def updatePassword(reqData: dict = Depends(requestDict)):
|
|||||||
return response
|
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="Проверить соединение с Медодс")
|
@router.post("/test-medods", name="testMedods", summary="Проверить соединение с Медодс")
|
||||||
async def testMedods(reqData: dict = Depends(requestDict)):
|
async def testMedods(reqData: dict = Depends(requestDict)):
|
||||||
logger.info(f"📥 Получен запрос на /test-medods")
|
logger.info(f"📥 Получен запрос на /test-medods")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
let systemStatus = {
|
let systemStatus = {
|
||||||
medods: { configured: false, details: {} },
|
medods: { configured: false, details: {} },
|
||||||
n3health: { configured: false, details: {} },
|
n3health: { configured: false, details: {} },
|
||||||
password: { configured: true }
|
delivery: { configured: false, types: {} } // Добавлен новый объект для способов доставки
|
||||||
};
|
};
|
||||||
|
|
||||||
// Глобальные переменные для статистики сотрудников
|
// Глобальные переменные для статистики сотрудников
|
||||||
@@ -44,6 +44,16 @@ async function loadSystemStatus() {
|
|||||||
hasLpuId: !!data.n3healthXIdLpu
|
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
|
// Обновляем UI
|
||||||
updateStatusUI();
|
updateStatusUI();
|
||||||
updateReadinessIndicator();
|
updateReadinessIndicator();
|
||||||
@@ -227,6 +237,50 @@ function updateStatusUI() {
|
|||||||
<div class="text-danger"><i class="bi bi-exclamation-circle"></i> Идентификатор МО: нет</div>
|
<div class="text-danger"><i class="bi bi-exclamation-circle"></i> Идентификатор МО: нет</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Способы доставки - с обновленными иконками
|
||||||
|
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 += `<div class="text-success mb-1"><i class="bi ${type.icon} me-1"></i> ${type.name} <span class="text-muted">(${type.desc})</span></div>`;
|
||||||
|
} else {
|
||||||
|
detailsHtml += `<div class="text-secondary mb-1"><i class="bi ${type.icon} me-1"></i> ${type.name} <span class="text-muted">(неактивен)</span></div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
deliveryDetails.innerHTML = detailsHtml;
|
||||||
|
} else {
|
||||||
|
deliveryBadge.className = 'badge bg-danger';
|
||||||
|
deliveryBadge.textContent = 'Не настроены';
|
||||||
|
|
||||||
|
deliveryDetails.innerHTML = `
|
||||||
|
<div class="text-danger mb-1"><i class="bi bi-chat-fill me-1"></i> MAX (чат) - неактивен</div>
|
||||||
|
<div class="text-danger mb-1"><i class="bi bi-envelope-fill me-1"></i> SMS - неактивен</div>
|
||||||
|
<div class="text-danger mb-1"><i class="bi bi-shield-fill me-1"></i> MILA - неактивен</div>
|
||||||
|
<div class="text-danger"><i class="bi bi-key-fill me-1"></i> GOSKEY - неактивен</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновление индикатора готовности и управление кнопкой
|
// Обновление индикатора готовности и управление кнопкой
|
||||||
@@ -236,9 +290,9 @@ function updateReadinessIndicator() {
|
|||||||
const settingsButtonContainer = document.getElementById('settingsButtonContainer');
|
const settingsButtonContainer = document.getElementById('settingsButtonContainer');
|
||||||
|
|
||||||
let configuredCount = 0;
|
let configuredCount = 0;
|
||||||
if (systemStatus.password.configured) configuredCount++;
|
|
||||||
if (systemStatus.medods.configured) configuredCount++;
|
if (systemStatus.medods.configured) configuredCount++;
|
||||||
if (systemStatus.n3health.configured) configuredCount++;
|
if (systemStatus.n3health.configured) configuredCount++;
|
||||||
|
if (systemStatus.delivery.configured) configuredCount++; // Заменили password на delivery
|
||||||
|
|
||||||
const readinessPercentage = Math.round((configuredCount / 3) * 100);
|
const readinessPercentage = Math.round((configuredCount / 3) * 100);
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,37 @@ let currentSettings = {
|
|||||||
host: '',
|
host: '',
|
||||||
authorization: '',
|
authorization: '',
|
||||||
xIdLpu: ''
|
xIdLpu: ''
|
||||||
}
|
},
|
||||||
|
delivery: { // Добавьте эту секцию
|
||||||
|
max: false,
|
||||||
|
sms: false,
|
||||||
|
mila: false,
|
||||||
|
goskey: false
|
||||||
|
},
|
||||||
|
multipleSending: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Состояния изменений
|
// Состояния изменений
|
||||||
let medodsChanged = false;
|
let medodsChanged = false;
|
||||||
let n3healthChanged = 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') {
|
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() {
|
function isMedodsFormValid() {
|
||||||
const host = document.getElementById('medodsHost').value;
|
const host = document.getElementById('medodsHost').value;
|
||||||
@@ -94,6 +159,30 @@ function updateStatusIndicators() {
|
|||||||
n3healthStatus.className = 'badge bg-danger';
|
n3healthStatus.className = 'badge bg-danger';
|
||||||
n3healthStatus.textContent = 'Требуется настройка';
|
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;
|
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();
|
updateStatusIndicators();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -264,6 +368,22 @@ function setupChangeListeners() {
|
|||||||
updateN3HealthButtons();
|
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
|
// Обработчик отмены изменений N3Health
|
||||||
document.getElementById('cancelN3HealthBtn').addEventListener('click', resetN3HealthForm);
|
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();
|
||||||
});
|
});
|
||||||
@@ -368,11 +368,17 @@ function updateStaffTable() {
|
|||||||
} else {
|
} else {
|
||||||
// Экранируем специальные символы в attorney
|
// Экранируем специальные символы в attorney
|
||||||
const attorney = (practitioner.attorney || '').replace(/"/g, '"').replace(/'/g, ''');
|
const attorney = (practitioner.attorney || '').replace(/"/g, '"').replace(/'/g, ''');
|
||||||
const eyeIcon = `<button type="button" class="btn btn-sm btn-link p-0 ms-1"
|
const eyeIcon = `<button type="button" class="btn btn-sm btn-link p-0 mx-1"
|
||||||
onclick="showAttorneyPopup('${attorney}', this)">
|
onclick="showAttorneyPopup('${attorney}', this)">
|
||||||
<i class="bi bi-eye"></i>
|
<i class="bi bi-eye"></i>
|
||||||
</button>`;
|
</button>`;
|
||||||
signatureTypeHtml = `<span class="badge bg-warning text-dark">МЧД</span>${eyeIcon}`;
|
const snils = (practitioner.snils || '').replace(/"/g, '"').replace(/'/g, ''');
|
||||||
|
const snilsIcon = `<button type="button" class="btn btn-sm btn-link p-0 ms-1"
|
||||||
|
onclick="showAttorneyPopup('${snils}', this)">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</button>`;
|
||||||
|
signatureTypeHtml = `<span class="badge bg-warning text-dark">МЧД</span>${eyeIcon}
|
||||||
|
<span class="badge bg-info">СНИЛС</span>${snilsIcon}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Форматируем дату и проверяем срок
|
// Форматируем дату и проверяем срок
|
||||||
@@ -630,6 +636,7 @@ function editPractitioner(userIdLpu, isEsiaAuth) {
|
|||||||
// Редактирование УКЭП
|
// Редактирование УКЭП
|
||||||
document.getElementById('ukepPractitionerId').value = userIdLpu;
|
document.getElementById('ukepPractitionerId').value = userIdLpu;
|
||||||
document.getElementById('ukepFile').required = false;
|
document.getElementById('ukepFile').required = false;
|
||||||
|
document.getElementById('ukepSnilsNumber').value = practitioner.snils;
|
||||||
|
|
||||||
// Очищаем data-атрибуты модального окна
|
// Очищаем data-атрибуты модального окна
|
||||||
const modal = document.getElementById('editUKEPModal');
|
const modal = document.getElementById('editUKEPModal');
|
||||||
@@ -650,6 +657,7 @@ function editPractitioner(userIdLpu, isEsiaAuth) {
|
|||||||
// Редактирование МЧД
|
// Редактирование МЧД
|
||||||
document.getElementById('mchdPractitionerId').value = userIdLpu;
|
document.getElementById('mchdPractitionerId').value = userIdLpu;
|
||||||
document.getElementById('mchdNumber').value = practitioner.attorney || '';
|
document.getElementById('mchdNumber').value = practitioner.attorney || '';
|
||||||
|
document.getElementById('mchdSnilsNumber').value = practitioner.snils;
|
||||||
|
|
||||||
// Очищаем data-атрибуты модального окна
|
// Очищаем data-атрибуты модального окна
|
||||||
const modal = document.getElementById('editMCHDModal');
|
const modal = document.getElementById('editMCHDModal');
|
||||||
@@ -779,6 +787,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
const userIdLpu = document.getElementById('mchdPractitionerId').value;
|
const userIdLpu = document.getElementById('mchdPractitionerId').value;
|
||||||
const mchdNumber = document.getElementById('mchdNumber').value.trim();
|
const mchdNumber = document.getElementById('mchdNumber').value.trim();
|
||||||
const expiryDate = document.getElementById('mchdExpiryDate').value;
|
const expiryDate = document.getElementById('mchdExpiryDate').value;
|
||||||
|
const snils = document.getElementById('mchdSnilsNumber').value.replace(/\D/g, '');
|
||||||
|
|
||||||
if (!mchdNumber || !expiryDate) {
|
if (!mchdNumber || !expiryDate) {
|
||||||
showAlert('Заполните все обязательные поля', 'warning');
|
showAlert('Заполните все обязательные поля', 'warning');
|
||||||
@@ -813,6 +822,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
userIdLpu: userIdLpu,
|
userIdLpu: userIdLpu,
|
||||||
esiaAuth: false,
|
esiaAuth: false,
|
||||||
attorney: mchdNumber,
|
attorney: mchdNumber,
|
||||||
|
snils: snils,
|
||||||
expired_at: expiryDate
|
expired_at: expiryDate
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -861,6 +871,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
requestBody = {
|
requestBody = {
|
||||||
userIdLpu: userIdLpu,
|
userIdLpu: userIdLpu,
|
||||||
attorney: mchdNumber,
|
attorney: mchdNumber,
|
||||||
|
snils: snils,
|
||||||
expired_at: expiryDate
|
expired_at: expiryDate
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -903,6 +914,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
const userIdLpu = document.getElementById('ukepPractitionerId').value;
|
const userIdLpu = document.getElementById('ukepPractitionerId').value;
|
||||||
const ukepFile = document.getElementById('ukepFile').files[0];
|
const ukepFile = document.getElementById('ukepFile').files[0];
|
||||||
const expiryDate = document.getElementById('ukepExpiryDate').value;
|
const expiryDate = document.getElementById('ukepExpiryDate').value;
|
||||||
|
const snils = document.getElementById('mchdSnilsNumber').value.replace(/\D/g, '');
|
||||||
|
|
||||||
if (!expiryDate) {
|
if (!expiryDate) {
|
||||||
showAlert('Заполните дату окончания действия', 'warning');
|
showAlert('Заполните дату окончания действия', 'warning');
|
||||||
@@ -942,6 +954,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
userIdLpu: userIdLpu,
|
userIdLpu: userIdLpu,
|
||||||
esiaAuth: true,
|
esiaAuth: true,
|
||||||
attorney: '',
|
attorney: '',
|
||||||
|
snils: snils,
|
||||||
expired_at: expiryDate
|
expired_at: expiryDate
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -991,6 +1004,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
userIdLpu: userIdLpu,
|
userIdLpu: userIdLpu,
|
||||||
|
snils: snils,
|
||||||
expired_at: expiryDate
|
expired_at: expiryDate
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,21 +18,21 @@
|
|||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Карточка настройки пароля -->
|
<!-- Карточка способов доставки сообщений -->
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="p-3 border rounded h-100">
|
<div class="p-3 border rounded h-100">
|
||||||
<div class="d-flex align-items-start mb-3">
|
<div class="d-flex align-items-start mb-3">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<i class="bi bi-shield-lock fs-4 text-primary"></i>
|
<i class="bi bi-chat-dots fs-4 text-primary"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow-1 ms-3">
|
<div class="flex-grow-1 ms-3">
|
||||||
<h6 class="mb-1">Пароль администратора</h6>
|
<h6 class="mb-1">Каналы доставки</h6>
|
||||||
<span id="passwordStatusBadge" class="badge bg-success">Настроен</span>
|
<span id="deliveryStatusBadge" class="badge bg-secondary">Не настроены</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-muted small mb-0">
|
<div id="deliveryDetails" class="small">
|
||||||
Пароль администратора установлен и защищает доступ к настройкам системы.
|
<!-- Динамически заполняемые детали -->
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,120 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Блок способов доставки -->
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#deliverySection">
|
||||||
|
<div class="d-flex align-items-center w-100">
|
||||||
|
<span class="me-3">Способы доставки</span>
|
||||||
|
<span id="deliveryStatus" class="badge bg-secondary">Загрузка...</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="deliverySection" class="accordion-collapse collapse" data-bs-parent="#settingsAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<form id="deliveryForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold">Доступные способы доставки (минимум один должен быть
|
||||||
|
выбран)</label>
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input delivery-checkbox" type="checkbox"
|
||||||
|
id="deliveryMax" value="max">
|
||||||
|
<label class="form-check-label" for="deliveryMax">
|
||||||
|
MAX
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input delivery-checkbox" type="checkbox"
|
||||||
|
id="deliverySms" value="sms">
|
||||||
|
<label class="form-check-label" for="deliverySms">
|
||||||
|
SMS
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input delivery-checkbox" type="checkbox"
|
||||||
|
id="deliveryMila" value="mila">
|
||||||
|
<label class="form-check-label" for="deliveryMila">
|
||||||
|
MILA
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input delivery-checkbox" type="checkbox"
|
||||||
|
id="deliveryGoskey" value="goskey">
|
||||||
|
<label class="form-check-label" for="deliveryGoskey">
|
||||||
|
GOSKEY
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="deliveryValidationMessage" class="form-text text-danger mt-2"
|
||||||
|
style="display: none;">
|
||||||
|
Должен быть выбран хотя бы один способ доставки
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" id="saveDeliveryBtn" class="btn btn-primary" disabled>
|
||||||
|
Сохранить настройки доставки
|
||||||
|
</button>
|
||||||
|
<button type="button" id="cancelDeliveryBtn" class="btn btn-outline-secondary" disabled>
|
||||||
|
Отменить изменения
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- После блока способов доставки, перед блоком Медодс -->
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#sendingSection">
|
||||||
|
<div class="d-flex align-items-center w-100">
|
||||||
|
<span class="me-3">Множественная отправка</span>
|
||||||
|
<span id="sendingStatus" class="badge bg-secondary">Загрузка...</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="sendingSection" class="accordion-collapse collapse" data-bs-parent="#settingsAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<form id="sendingForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="multipleSending" role="switch">
|
||||||
|
<label class="form-check-label fw-bold" for="multipleSending">
|
||||||
|
Отправка нескольким получателям
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-text text-muted mt-2">
|
||||||
|
Включение и отключение возможности отправлять на подписание одновременно нескольким
|
||||||
|
получателям.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" id="saveSendingBtn" class="btn btn-primary" disabled>
|
||||||
|
Сохранить настройки отправки
|
||||||
|
</button>
|
||||||
|
<button type="button" id="cancelSendingBtn" class="btn btn-outline-secondary" disabled>
|
||||||
|
Отменить изменения
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Блок Медодс -->
|
<!-- Блок Медодс -->
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header">
|
<h2 class="accordion-header">
|
||||||
|
|||||||
@@ -164,6 +164,10 @@
|
|||||||
<label for="mchdExpiryDate" class="form-label">Дата окончания действия</label>
|
<label for="mchdExpiryDate" class="form-label">Дата окончания действия</label>
|
||||||
<input type="date" class="form-control" id="mchdExpiryDate" required>
|
<input type="date" class="form-control" id="mchdExpiryDate" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="mchdSnilsNumber" class="form-label">СНИЛС (только цифры)</label>
|
||||||
|
<input type="text" class="form-control" id="mchdSnilsNumber" required>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -193,6 +197,10 @@
|
|||||||
<label for="ukepExpiryDate" class="form-label">Дата окончания действия</label>
|
<label for="ukepExpiryDate" class="form-label">Дата окончания действия</label>
|
||||||
<input type="date" class="form-control" id="ukepExpiryDate" required>
|
<input type="date" class="form-control" id="ukepExpiryDate" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="ukepSnilsNumber" class="form-label">СНИЛС (только цифры)</label>
|
||||||
|
<input type="text" class="form-control" id="ukepSnilsNumber" required>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
some_data
|
|
||||||
@@ -66,7 +66,7 @@ class Patient(Base):
|
|||||||
f"{kwargs.get('familyName', '')} {kwargs.get('givenName', '')} {kwargs.get('middleName', '')}"
|
f"{kwargs.get('familyName', '')} {kwargs.get('givenName', '')} {kwargs.get('middleName', '')}"
|
||||||
)
|
)
|
||||||
kwargs["birthDate"] = datetime.fromisoformat(kwargs.pop("birthDate"))
|
kwargs["birthDate"] = datetime.fromisoformat(kwargs.pop("birthDate"))
|
||||||
utils.logger.info(
|
utils.logger.debug(
|
||||||
f"Добавление пациента {kwargs.get('name', '')} с идентификатором {idPatientMis}"
|
f"Добавление пациента {kwargs.get('name', '')} с идентификатором {idPatientMis}"
|
||||||
)
|
)
|
||||||
patient = Patient(**kwargs)
|
patient = Patient(**kwargs)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class Practitioner(Base):
|
|||||||
fullName = Column(String)
|
fullName = Column(String)
|
||||||
esiaAuth = Column(Boolean)
|
esiaAuth = Column(Boolean)
|
||||||
attorney = Column(String, nullable=True)
|
attorney = Column(String, nullable=True)
|
||||||
|
snils = Column(String)
|
||||||
expired_at = Column(DateTime)
|
expired_at = Column(DateTime)
|
||||||
created_at = Column(DateTime, default=datetime.now)
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import secrets
|
import secrets
|
||||||
from sqlalchemy import Column, DateTime, Integer, String, select
|
from sqlalchemy import Boolean, Column, DateTime, Integer, String, select
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
from db import Base, CRUD
|
from db import Base, CRUD
|
||||||
from argon2 import PasswordHasher
|
from argon2 import PasswordHasher
|
||||||
from argon2.exceptions import VerifyMismatchError
|
from argon2.exceptions import VerifyMismatchError
|
||||||
@@ -23,7 +24,11 @@ class Settings(Base):
|
|||||||
n3healthHost = Column(String, nullable=True)
|
n3healthHost = Column(String, nullable=True)
|
||||||
n3healthAutorization = Column(String, nullable=True)
|
n3healthAutorization = Column(String, nullable=True)
|
||||||
n3healthXIdLpu = Column(String, nullable=True)
|
n3healthXIdLpu = Column(String, nullable=True)
|
||||||
expirationAlert = Column(Integer, nullable=True)
|
expirationAlert = Column(Integer, default=45)
|
||||||
|
deliveryType = Column(
|
||||||
|
JSONB, default={"sms": True, "max": True, "mila": False, "goskey": False}
|
||||||
|
)
|
||||||
|
multipleSending = Column(Boolean, default=False)
|
||||||
created_at = Column(DateTime, default=datetime.now)
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
@@ -69,7 +74,6 @@ class Settings(Base):
|
|||||||
"n3healthHost": "https://b2b-demo.n3health.ru/sep/api",
|
"n3healthHost": "https://b2b-demo.n3health.ru/sep/api",
|
||||||
"n3healthAutorization": "42e2080c-e33f-0800-3080-4e6989e3d97f",
|
"n3healthAutorization": "42e2080c-e33f-0800-3080-4e6989e3d97f",
|
||||||
"n3healthXIdLpu": "8fc38b60-035b-462d-ad1b-31863e0fd2f0",
|
"n3healthXIdLpu": "8fc38b60-035b-462d-ad1b-31863e0fd2f0",
|
||||||
"expirationAlert": 45,
|
|
||||||
}
|
}
|
||||||
settings = Settings(**settingsData)
|
settings = Settings(**settingsData)
|
||||||
data = await settings.save()
|
data = await settings.save()
|
||||||
|
|||||||
+55
-22
@@ -1,5 +1,6 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func, select
|
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func, select
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from db import Base, CRUD
|
from db import Base, CRUD
|
||||||
from utils import logger, answer, toDict
|
from utils import logger, answer, toDict
|
||||||
@@ -15,8 +16,10 @@ class Signing(Base):
|
|||||||
idPatientMis = Column(
|
idPatientMis = Column(
|
||||||
String, ForeignKey("patients.idPatientMis", ondelete="CASCADE")
|
String, ForeignKey("patients.idPatientMis", ondelete="CASCADE")
|
||||||
)
|
)
|
||||||
storagePath = Column(String, nullable=True)
|
recipients = Column(JSONB, default=[])
|
||||||
|
storagePath = Column(JSONB, default=[])
|
||||||
trackingId = Column(String, nullable=True)
|
trackingId = Column(String, nullable=True)
|
||||||
|
deliveryType = Column(String)
|
||||||
documents = relationship(
|
documents = relationship(
|
||||||
"Document",
|
"Document",
|
||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
@@ -51,7 +54,9 @@ class Signing(Base):
|
|||||||
return await CRUD.delete(self)
|
return await CRUD.delete(self)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def addSigning(userIdLpu: str, idPatientMis: str):
|
async def addSigning(
|
||||||
|
userIdLpu: str, idPatientMis: str, deliveryType: str, recipients: list = []
|
||||||
|
):
|
||||||
from utils.background import enable_job
|
from utils.background import enable_job
|
||||||
|
|
||||||
if type(userIdLpu) != str:
|
if type(userIdLpu) != str:
|
||||||
@@ -61,7 +66,12 @@ class Signing(Base):
|
|||||||
|
|
||||||
enable_job()
|
enable_job()
|
||||||
|
|
||||||
return await Signing(userIdLpu=userIdLpu, idPatientMis=idPatientMis).save()
|
return await Signing(
|
||||||
|
userIdLpu=userIdLpu,
|
||||||
|
idPatientMis=idPatientMis,
|
||||||
|
deliveryType=deliveryType,
|
||||||
|
recipients=recipients,
|
||||||
|
).save()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def getSigningById(id: int, toDict: bool = True):
|
async def getSigningById(id: int, toDict: bool = True):
|
||||||
@@ -76,14 +86,36 @@ class Signing(Base):
|
|||||||
async def getSigningsByIdPatientMis(idPatientMis: str, toDict: bool = True):
|
async def getSigningsByIdPatientMis(idPatientMis: str, toDict: bool = True):
|
||||||
if type(idPatientMis) != str:
|
if type(idPatientMis) != str:
|
||||||
idPatientMis = str(idPatientMis)
|
idPatientMis = str(idPatientMis)
|
||||||
|
|
||||||
|
# Поиск подписаний, где idPatientMis является прямым значением поля idPatientMis
|
||||||
signings = await CRUD.read(
|
signings = await CRUD.read(
|
||||||
select(Signing).where(Signing.idPatientMis == idPatientMis), True
|
select(Signing).where(Signing.idPatientMis == idPatientMis),
|
||||||
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data = []
|
||||||
if signings:
|
if signings:
|
||||||
data = [signing.toDict() for signing in signings] if toDict else signings
|
data.extend(
|
||||||
|
[signing.toDict() for signing in signings] if toDict else signings
|
||||||
|
)
|
||||||
|
|
||||||
|
signings2 = await CRUD.read(
|
||||||
|
select(Signing).where(Signing.recipients.contains([idPatientMis])),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Добавляем только те записи из signings2, которых еще нет в data (чтобы избежать дубликатов)
|
||||||
|
if signings2:
|
||||||
|
existing_ids = {s.id for s in data if hasattr(s, "id")}
|
||||||
|
for signing in signings2:
|
||||||
|
if signing.id not in existing_ids:
|
||||||
|
data.append(signing.toDict() if toDict else signing)
|
||||||
|
|
||||||
|
if len(data) > 0:
|
||||||
return answer(data=data)
|
return answer(data=data)
|
||||||
logger.error(f"Подписания не найдены, idPatientMis: {idPatientMis}")
|
|
||||||
return answer(success=False, message="Подписания не найдены")
|
logger.error(f"Подписания не найдены, idPatientMis: {idPatientMis}")
|
||||||
|
return answer(success=False, message="Подписания не найдены")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def getSigningsByUserIdLpu(userIdLpu: str, toDict: bool = True):
|
async def getSigningsByUserIdLpu(userIdLpu: str, toDict: bool = True):
|
||||||
@@ -116,19 +148,15 @@ class Signing(Base):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def updateStatuses():
|
async def updateStatuses():
|
||||||
checkList = []
|
|
||||||
|
|
||||||
singingsInProgress = await CRUD.read(
|
singingsInProgress = await CRUD.read(
|
||||||
select(Signing).where(Signing.storagePath == None), True
|
select(Signing).where(
|
||||||
|
func.jsonb_array_length(Signing.storagePath) == 0,
|
||||||
|
Signing.trackingId != None,
|
||||||
|
),
|
||||||
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if singingsInProgress and len(singingsInProgress) > 0:
|
checkList = [singing.id for singing in singingsInProgress]
|
||||||
for signing in singingsInProgress:
|
|
||||||
if len(signing.statuses) > 0:
|
|
||||||
maxCode = max([status.status for status in signing.statuses])
|
|
||||||
if maxCode >= 204:
|
|
||||||
continue
|
|
||||||
checkList.append(signing.id)
|
|
||||||
|
|
||||||
logger.info(f"Проверка статусов для {len(checkList)} подписаний")
|
logger.info(f"Проверка статусов для {len(checkList)} подписаний")
|
||||||
|
|
||||||
@@ -198,8 +226,13 @@ class Signing(Base):
|
|||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
deliveryType = filters.get("deliveryType")
|
||||||
|
|
||||||
query = select(Signing)
|
query = select(Signing)
|
||||||
|
|
||||||
|
if deliveryType is not None and deliveryType != "all":
|
||||||
|
query = query.where(Signing.deliveryType == deliveryType)
|
||||||
|
|
||||||
if userFilter is not None:
|
if userFilter is not None:
|
||||||
if userFilter: # True - только свои
|
if userFilter: # True - только свои
|
||||||
query = query.where(Signing.userIdLpu == userIdLpu)
|
query = query.where(Signing.userIdLpu == userIdLpu)
|
||||||
@@ -223,17 +256,17 @@ class Signing(Base):
|
|||||||
result = [
|
result = [
|
||||||
signing
|
signing
|
||||||
for signing in result
|
for signing in result
|
||||||
if (signing.storagePath is None and signing.trackingId is not None)
|
if (
|
||||||
|
len(signing.storagePath) == 0 and signing.trackingId is not None
|
||||||
|
)
|
||||||
]
|
]
|
||||||
case "completed":
|
case "completed":
|
||||||
result = [
|
result = [signing for signing in result if len(signing.storagePath) > 0]
|
||||||
signing for signing in result if signing.storagePath is not None
|
|
||||||
]
|
|
||||||
case "error":
|
case "error":
|
||||||
result = [
|
result = [
|
||||||
signing
|
signing
|
||||||
for signing in result
|
for signing in result
|
||||||
if (signing.trackingId is None and signing.storagePath is None)
|
if (signing.trackingId is None and len(signing.storagePath) == 0)
|
||||||
]
|
]
|
||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
|
|||||||
+81
-26
@@ -1,6 +1,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, select
|
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, select
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
from db import CRUD, Base
|
from db import CRUD, Base
|
||||||
from db.schemas.signings import Signing
|
from db.schemas.signings import Signing
|
||||||
from utils import answer, logger, toDict
|
from utils import answer, logger, toDict
|
||||||
@@ -11,6 +11,18 @@ class Statuses(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
singingId = Column(Integer, ForeignKey("signings.id", ondelete="CASCADE"))
|
singingId = Column(Integer, ForeignKey("signings.id", ondelete="CASCADE"))
|
||||||
|
idPatientMis = Column(
|
||||||
|
String,
|
||||||
|
ForeignKey("patients.idPatientMis", ondelete="CASCADE"),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
patient = relationship(
|
||||||
|
"Patient",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
lazy="joined",
|
||||||
|
uselist=False,
|
||||||
|
single_parent=True,
|
||||||
|
)
|
||||||
status = Column(Integer)
|
status = Column(Integer)
|
||||||
created_at = Column(DateTime, default=datetime.now)
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
|
|
||||||
@@ -28,12 +40,16 @@ class Statuses(Base):
|
|||||||
return await CRUD.update(Statuses, self.id, **kwargs)
|
return await CRUD.update(Statuses, self.id, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def addStatus(singingId: int, status: int):
|
async def addStatus(singingId: int, status: int, idPatientMis: str = None):
|
||||||
if type(singingId) != int:
|
if type(singingId) != int:
|
||||||
singingId = int(singingId)
|
singingId = int(singingId)
|
||||||
if type(status) != int:
|
if type(status) != int:
|
||||||
status = int(status)
|
status = int(status)
|
||||||
await CRUD.create(Statuses(singingId=singingId, status=status))
|
if idPatientMis is not None and type(idPatientMis) != str:
|
||||||
|
idPatientMis = str(idPatientMis)
|
||||||
|
await CRUD.create(
|
||||||
|
Statuses(singingId=singingId, status=status, idPatientMis=idPatientMis)
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def getStatus(singingId: int) -> list:
|
async def getStatus(singingId: int) -> list:
|
||||||
@@ -58,36 +74,75 @@ class Statuses(Base):
|
|||||||
logger.error(singingData.message)
|
logger.error(singingData.message)
|
||||||
return singingData
|
return singingData
|
||||||
|
|
||||||
statusDB = await Statuses.getStatus(singingId)
|
statusesDB = await Statuses.getStatus(singingId)
|
||||||
latestStatusCode = 0
|
|
||||||
try:
|
|
||||||
latestStatusCode = statusDB[-1].status
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
statusData = await getTrackingIDSigning(
|
latestStatusData = {}
|
||||||
singingData.data.trackingId, latestStatusCode, singingId
|
|
||||||
)
|
if len(statusesDB) > 0:
|
||||||
if not statusData.success:
|
for statusDB in statusesDB:
|
||||||
|
idPatientMis = statusDB.idPatientMis
|
||||||
|
if not idPatientMis:
|
||||||
|
idPatientMis = "global"
|
||||||
|
if idPatientMis not in latestStatusData:
|
||||||
|
latestStatusData[idPatientMis] = 0
|
||||||
|
if statusDB.status > latestStatusData[idPatientMis]:
|
||||||
|
latestStatusData[idPatientMis] = statusDB.status
|
||||||
|
|
||||||
|
statusData = await getTrackingIDSigning(singingData.data.trackingId, singingId)
|
||||||
|
if not statusData.success and not isNew:
|
||||||
logger.error(statusData.message)
|
logger.error(statusData.message)
|
||||||
return statusData
|
return statusData
|
||||||
|
|
||||||
actualStatusCode = int(statusData.data["code"])
|
actualStatusData = {}
|
||||||
if actualStatusCode == 0 and isNew:
|
|
||||||
actualStatusCode = 201
|
if statusData.data:
|
||||||
|
actualStatusData = statusData.data
|
||||||
|
elif isNew:
|
||||||
|
actualStatusData = {"global": 207, singingData.data.idPatientMis: 201}
|
||||||
|
for recipient in singingData.data.recipients:
|
||||||
|
actualStatusData[recipient] = 201
|
||||||
|
|
||||||
|
if not actualStatusData:
|
||||||
|
logger.error("Нет данных о статусе")
|
||||||
|
return answer()
|
||||||
|
|
||||||
|
for idPatientMis, actualStatusCode in actualStatusData.items():
|
||||||
|
if idPatientMis == "storagePath":
|
||||||
|
continue
|
||||||
|
latestStatusCode = latestStatusData.get(idPatientMis, 0)
|
||||||
|
|
||||||
if latestStatusCode != actualStatusCode:
|
if latestStatusCode != actualStatusCode:
|
||||||
logger.info(f"✔️ Обновился статус: {singingId} - {actualStatusCode}")
|
logger.info(
|
||||||
await Statuses.addStatus(singingId, actualStatusCode)
|
f"⚠️ Обновился статус подписания #{singingId} для '{idPatientMis}': {actualStatusCode}"
|
||||||
else:
|
)
|
||||||
logger.info(f"📌 Статус не изменился: {singingId} - {actualStatusCode}")
|
await Statuses.addStatus(
|
||||||
|
singingId,
|
||||||
|
actualStatusCode,
|
||||||
|
idPatientMis if idPatientMis != "global" else None,
|
||||||
|
)
|
||||||
|
|
||||||
if actualStatusCode == 204:
|
if (
|
||||||
logger.info(f"✅ Подписание завершено: {singingId}")
|
"storagePath" in actualStatusData
|
||||||
await singingData.data.edit(storagePath=statusData.data["storagePath"])
|
and len(actualStatusData["storagePath"]) > 0
|
||||||
|
):
|
||||||
|
logger.info(f"✅ Подписание #{singingId} завершено успешно")
|
||||||
|
await singingData.data.edit(
|
||||||
|
storagePath=actualStatusData["storagePath"]
|
||||||
|
)
|
||||||
|
|
||||||
if actualStatusCode > 204:
|
elif actualStatusCode in [213, 498, 499, 500, 501]:
|
||||||
logger.info(f"❌ Подписание не успешно: {statusData.data['description']}")
|
logger.warning(
|
||||||
|
f"❌ Подписание #{singingId} завершено не успешно: {actualStatusCode}"
|
||||||
|
)
|
||||||
await singingData.data.edit(trackingId=None)
|
await singingData.data.edit(trackingId=None)
|
||||||
|
|
||||||
|
elif actualStatusCode in [205, 206, 212]:
|
||||||
|
logger.warning(
|
||||||
|
f"❌ Подписание #{singingId} завершено не успешно для '{idPatientMis}': {actualStatusCode}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f"📌 Не обновился статус подписания #{singingId} для '{idPatientMis}': {latestStatusCode}"
|
||||||
|
)
|
||||||
|
|
||||||
return answer()
|
return answer()
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ async def requestPOST(
|
|||||||
async with session.post(
|
async with session.post(
|
||||||
url, json=json, headers=headers, **kwargs
|
url, json=json, headers=headers, **kwargs
|
||||||
) as response:
|
) as response:
|
||||||
try:
|
# try:
|
||||||
responseJson = await response.json()
|
# responseJson = await response.json()
|
||||||
logger.warning(responseJson)
|
# logger.warning(responseJson)
|
||||||
except:
|
# except:
|
||||||
logger.error("Не удалось распарсить ответ сервера")
|
# logger.error("Не удалось распарсить ответ сервера")
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
try:
|
try:
|
||||||
return await response.json()
|
return await response.json()
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ def detect_and_save_attachment(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
mime_type = "application/octet-stream"
|
mime_type = "application/octet-stream"
|
||||||
extension = ".bin"
|
extension = ".sig"
|
||||||
|
|
||||||
for signature, detected_mime, detected_ext in ATTACHMENT_SIGNATURES:
|
for signature, detected_mime, detected_ext in ATTACHMENT_SIGNATURES:
|
||||||
if raw.startswith(signature):
|
if raw.startswith(signature):
|
||||||
|
|||||||
+15
-69
@@ -13,11 +13,10 @@ from utils import logger
|
|||||||
|
|
||||||
|
|
||||||
class WeasyPrintConverter:
|
class WeasyPrintConverter:
|
||||||
def __init__(self, output_dirs: list[str]):
|
def __init__(self, output_dir: list[str]):
|
||||||
self.output_dirs = [Path(d).resolve() for d in output_dirs]
|
self.output_dir = Path(output_dir).resolve()
|
||||||
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.max_files = 5
|
self.max_files = 5
|
||||||
for output_dir in self.output_dirs:
|
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
self.weasyprint_path = self._find_weasyprint()
|
self.weasyprint_path = self._find_weasyprint()
|
||||||
|
|
||||||
@@ -428,7 +427,7 @@ class WeasyPrintConverter:
|
|||||||
</html>"""
|
</html>"""
|
||||||
|
|
||||||
def convert_html_to_pdf(
|
def convert_html_to_pdf(
|
||||||
self, html_content: str, output_paths: list[Path]
|
self, html_content: str, output_path: Path
|
||||||
) -> Tuple[bool, str]:
|
) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
Конвертирует HTML в PDF и сохраняет во все указанные папки
|
Конвертирует HTML в PDF и сохраняет во все указанные папки
|
||||||
@@ -447,24 +446,15 @@ class WeasyPrintConverter:
|
|||||||
temp_html = f.name
|
temp_html = f.name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Создаем временный PDF
|
|
||||||
with tempfile.NamedTemporaryFile(
|
|
||||||
suffix=".pdf", delete=False
|
|
||||||
) as temp_pdf_file:
|
|
||||||
temp_pdf = temp_pdf_file.name
|
|
||||||
|
|
||||||
# Конвертируем во временный файл
|
|
||||||
cmd = [
|
cmd = [
|
||||||
self.weasyprint_path,
|
self.weasyprint_path,
|
||||||
temp_html,
|
temp_html,
|
||||||
temp_pdf,
|
output_path,
|
||||||
"--encoding",
|
"--encoding",
|
||||||
"utf-8",
|
"utf-8",
|
||||||
"--presentational-hints",
|
"--presentational-hints",
|
||||||
]
|
]
|
||||||
|
|
||||||
logger.debug("Выполняем однократную конвертацию WeasyPrint")
|
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
cmd, capture_output=True, text=True, timeout=180
|
cmd, capture_output=True, text=True, timeout=180
|
||||||
)
|
)
|
||||||
@@ -475,54 +465,14 @@ class WeasyPrintConverter:
|
|||||||
)
|
)
|
||||||
return False, f"Ошибка конвертации: {error_msg}"
|
return False, f"Ошибка конвертации: {error_msg}"
|
||||||
|
|
||||||
# Проверяем временный PDF
|
if not output_path.exists() or output_path.stat().st_size < 1024:
|
||||||
temp_pdf_path = Path(temp_pdf)
|
|
||||||
if not temp_pdf_path.exists() or temp_pdf_path.stat().st_size < 1024:
|
|
||||||
return False, "Созданный PDF слишком мал или отсутствует"
|
return False, "Созданный PDF слишком мал или отсутствует"
|
||||||
|
|
||||||
# Копируем во все целевые папки
|
|
||||||
successful_files = []
|
|
||||||
failed_files = []
|
|
||||||
|
|
||||||
for output_path in output_paths:
|
|
||||||
try:
|
|
||||||
# Создаем родительскую папку
|
|
||||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Копируем файл
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
shutil.copy2(temp_pdf, output_path)
|
|
||||||
|
|
||||||
# Проверяем результат
|
|
||||||
if output_path.exists() and output_path.stat().st_size > 1024:
|
|
||||||
successful_files.append(str(output_path))
|
|
||||||
logger.debug(f"✅ PDF сохранен: {output_path}")
|
|
||||||
else:
|
|
||||||
failed_files.append(
|
|
||||||
f"{output_path}: файл поврежден или слишком мал"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
failed_files.append(f"{output_path}: {str(e)}")
|
|
||||||
logger.error(f"❌ Ошибка копирования в {output_path}: {str(e)}")
|
|
||||||
|
|
||||||
# Удаляем временный PDF
|
|
||||||
os.unlink(temp_pdf)
|
|
||||||
|
|
||||||
# Формируем результат
|
# Формируем результат
|
||||||
if successful_files and not failed_files:
|
|
||||||
return (
|
return (
|
||||||
True,
|
True,
|
||||||
f"✅ PDF создан и сохранен во все {len(successful_files)} папок",
|
f"✅ PDF создан и сохранен: {output_path}",
|
||||||
)
|
)
|
||||||
elif successful_files:
|
|
||||||
return (
|
|
||||||
True,
|
|
||||||
f"⚠️ PDF создан, но сохранен только в {len(successful_files)}/{len(output_paths)} папок",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return False, f"❌ Не удалось сохранить PDF ни в одну папку"
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if os.path.exists(temp_html):
|
if os.path.exists(temp_html):
|
||||||
@@ -537,7 +487,7 @@ class WeasyPrintConverter:
|
|||||||
def convert_documents(self, docs: List[Dict]) -> Dict[str, Dict]:
|
def convert_documents(self, docs: List[Dict]) -> Dict[str, Dict]:
|
||||||
results = {
|
results = {
|
||||||
"status": "SUCCESS",
|
"status": "SUCCESS",
|
||||||
"files": {},
|
"files": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(docs) > self.max_files:
|
if len(docs) > self.max_files:
|
||||||
@@ -553,17 +503,13 @@ class WeasyPrintConverter:
|
|||||||
|
|
||||||
safe_title = self._clean_filename(doc_title)
|
safe_title = self._clean_filename(doc_title)
|
||||||
filename = f"{doc_number}_{safe_title}.pdf"
|
filename = f"{doc_number}_{safe_title}.pdf"
|
||||||
output_paths = [output_dir / filename for output_dir in self.output_dirs]
|
output_path = self.output_dir / filename
|
||||||
|
|
||||||
for path in self.output_dirs:
|
results["files"].append(
|
||||||
key = path.parts[-1]
|
|
||||||
if key not in results["files"]:
|
|
||||||
results["files"][key] = []
|
|
||||||
results["files"][key].append(
|
|
||||||
{
|
{
|
||||||
"number": int(doc_number),
|
"number": int(doc_number),
|
||||||
"title": doc_title,
|
"title": doc_title,
|
||||||
"storagePath": str(path / filename),
|
"storagePath": str(output_path),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -574,7 +520,7 @@ class WeasyPrintConverter:
|
|||||||
logger.warning(f" ⚠ Пропущен: недостаточно контента")
|
logger.warning(f" ⚠ Пропущен: недостаточно контента")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
success, message = self.convert_html_to_pdf(html_content, output_paths)
|
success, message = self.convert_html_to_pdf(html_content, output_path)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
logger.info(f" ✅ Успешно")
|
logger.info(f" ✅ Успешно")
|
||||||
@@ -584,11 +530,11 @@ class WeasyPrintConverter:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def convert_docs_to_pdfs(docs: List[Dict], patientsData: list[dict]) -> Dict:
|
def convert_docs_to_pdfs(docs: List[Dict], idPatientMis: str) -> Dict:
|
||||||
try:
|
try:
|
||||||
baseDir = "src/attachments/export/"
|
baseDir = "src/attachments/export/"
|
||||||
outputDirs = [f"{baseDir}{patient['idPatientMis']}" for patient in patientsData]
|
outputDir = f"{baseDir}{idPatientMis}"
|
||||||
converter = WeasyPrintConverter(outputDirs)
|
converter = WeasyPrintConverter(outputDir)
|
||||||
return converter.convert_documents(docs)
|
return converter.convert_documents(docs)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user