diff --git a/.DS_Store b/.DS_Store index dc79978..2ebd6f5 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/background.js b/background.js index b20df98..179f0fa 100644 --- a/background.js +++ b/background.js @@ -36,7 +36,7 @@ const statuses = { category: "error" }, 207: { - name: "Ожидание действий всех получателей", + name: "Ожидание действий", description: "Ожидание действий от всех получателей", category: "processing" }, @@ -70,6 +70,11 @@ const statuses = { description: "Подписание отменено для всех получателей", category: "completed" }, + 214: { + name: "Успешно. Требуется подписание УКЭП", + description: "Требуется подписание УКЭП медицинского работника", + category: "processing" + }, 498: { name: "Завершено. Отклонено всеми", description: "Все получатели отказались от подписания", @@ -315,6 +320,23 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { return true; } + if (message.action === 'searchRecipients') { + setTimeout(async () => { + try { + const result = await searchRecipients(message.data); + sendResponse(result); + } catch (error) { + sendResponse({ + success: false, + message: error.message || 'Ошибка поиска получателей', + error: error.toString(), + data: [] + }); + } + }, 0); + return true; + } + }); // Функция расширенного поиска @@ -442,8 +464,8 @@ async function getDocumentsFromServer(data) { const result = await response.json(); - if (result.status === 'SUCCESS' && Array.isArray(result.data)) { - result.data = enrichStatuses(result.data); + if (result.status === 'SUCCESS' && Array.isArray(result.data.singings)) { + result.data.singings = enrichStatuses(result.data.singings); } return { @@ -616,4 +638,56 @@ async function resendDocumentsOnServer(data) { } } +async function searchRecipients(data) { + try { + const settings = await chrome.storage.local.get(['serverIp', 'serverPort']); + + if (!settings.serverIp || !settings.serverPort) { + throw new Error('Server IP or port not configured'); + } + + const url = `http://${settings.serverIp}:${settings.serverPort}/api/search-recipients`; + + console.log('[background] Поиск получателей:', url, data); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify(data), + signal: AbortSignal.timeout(15000) // 15 секунд таймаут + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP error: ${response.status} - ${errorText}`); + } + + const result = await response.json(); + + let recipients = result; + if (result.data && Array.isArray(result.data)) { + recipients = result.data; + } else if (!Array.isArray(result)) { + recipients = []; + } + + return { + success: true, + data: recipients + }; + + } catch (error) { + console.error('[background] Ошибка поиска получателей:', error); + return { + success: false, + message: error.message || 'Ошибка поиска получателей', + error: error.toString(), + data: [] + }; + } +} + console.log('✅ Background script loaded'); \ No newline at end of file diff --git a/content.js b/content.js index 8b8a811..2435a33 100644 --- a/content.js +++ b/content.js @@ -40,6 +40,8 @@ window.addEventListener('message', (event) => { async function loadPageData() { storageData = await chrome.storage.local.get(dataType); + const userData = await chrome.storage.local.get(userDataType); + storageData.userData = userData[userDataType]; if (!storageData || !storageData.metaData || Object.keys(storageData.metaData).length === 0) { console.log(`[EXT][content] no data for ${dataType}, injecting...`); @@ -101,8 +103,8 @@ window.addEventListener('message', async (event) => { } // Подготавливаем данные для отправки - const userData = storageData.metaData.userData; - const patientId = storageData.metaData.patients?.[0]?.idPatientMis; + const userData = storageData.userData; + const patientId = storageData.metaData.patient.idPatientMis; if (!userData || !patientId) { window.postMessage({ @@ -156,10 +158,6 @@ window.addEventListener('message', async (event) => { isProcessing = true; - console.log('[EXT][content] Forwarding to background:', { - docNumbers: event.data.payload.docNumbers - }); - // Немедленный ответ о принятии задачи window.postMessage({ source: 'medods-extension', @@ -178,8 +176,6 @@ window.addEventListener('message', async (event) => { event.data.payload.docNumbers.includes(parseInt(doc.number, 10)) ); - console.log(`📄 [Content] Найдено документов для отправки: ${filteredDocs.length}`); - if (filteredDocs.length === 0) { window.postMessage({ source: 'medods-extension', @@ -198,8 +194,10 @@ window.addEventListener('message', async (event) => { // Собираем все данные для отправки const sendData = { practitioner: storageData.metaData.practitioner, - patients: storageData.metaData.patients, + patient: storageData.metaData.patient, + recipients: event.data.payload.recipients, docs: filteredDocs, + deliveryType: event.data.payload.deliveryType }; // Асинхронная обработка в background @@ -243,7 +241,7 @@ window.addEventListener('message', async (event) => { } // Получаем idPatientMis из метаданных - const idPatientMis = storageData.metaData.patients?.[0]?.idPatientMis; + const idPatientMis = storageData.metaData.patient.idPatientMis; if (!idPatientMis) { window.postMessage({ @@ -289,7 +287,7 @@ window.addEventListener('message', async (event) => { // ОБРАБОТЧИК ПРОВЕРКИ ПРАВ НА ОТЗЫВ if (event.data.type === 'checkRevokePermission') { - const hasPermission = String(storageData.metaData?.userData?.id) === String(event.data.payload.userIdLpu); + const hasPermission = String(storageData.userData?.id) === String(event.data.payload.userIdLpu); window.postMessage({ source: 'medods-extension', @@ -302,8 +300,6 @@ window.addEventListener('message', async (event) => { // ОБРАБОТЧИК ОТЗЫВА ДОКУМЕНТОВ if (event.data.type === 'revokeDocuments') { - console.log('[EXT][content] Запрос на отзыв документов:', event.data.payload); - chrome.runtime.sendMessage({ action: 'revokeDocuments', data: event.data.payload @@ -318,8 +314,6 @@ window.addEventListener('message', async (event) => { // ОБРАБОТЧИК ПОВТОРНОЙ ОТПРАВКИ ДОКУМЕНТОВ if (event.data.type === 'resendDocuments') { - console.log('[EXT][content] Запрос на повторную отправку документов:', event.data.payload); - chrome.runtime.sendMessage({ action: 'resendDocuments', data: event.data.payload @@ -332,6 +326,27 @@ window.addEventListener('message', async (event) => { }); } + // ОБРАБОТЧИК ПОИСКА ПОЛУЧАТЕЛЕЙ ПО ТЕЛЕФОНУ + if (event.data.type === 'searchRecipients') { + const data = { + ...event.data.payload, + idPatientMis: storageData.metaData.patient.idPatientMis + }; + + // Отправляем запрос в background + chrome.runtime.sendMessage({ + action: 'searchRecipients', + data: data + }, (response) => { + // Пересылаем результат обратно в singing.js + window.postMessage({ + source: 'medods-extension', + type: 'searchRecipientsResponse', + payload: response + }, '*'); + }); + } + }); // Основной цикл diff --git a/documents.js b/documents.js index 08d1459..69bac0b 100644 --- a/documents.js +++ b/documents.js @@ -4,6 +4,7 @@ // Глобальная переменная для хранения текущего уведомления let currentMessageEl = null; let messageTimeout = null; + let deliveryTypeSettings = {}; // Функция для отображения сообщений function showMessage(message, type = 'success', duration = 3000) { @@ -136,45 +137,81 @@ modalWrapper.className = 'el-dialog__wrapper'; modalWrapper.style.cssText = 'z-index: 2001; position: fixed; top: 0; right: 0; bottom: 0; left: 0; overflow: auto;'; - const backdrop = document.createElement('div'); - backdrop.className = 'v-modal'; - backdrop.style.cssText = 'z-index: 2001;'; - backdrop.tabIndex = 0; + // Разделяем статусы на общие и по получателям + const commonStatuses = singing.statuses.filter(s => s.idPatientMis === null); + const recipientStatuses = singing.statuses.filter(s => s.idPatientMis !== null); - const sortedStatuses = [...singing.statuses].sort((a, b) => a.id - b.id); + // Группируем статусы по получателям + const statusesByRecipient = {}; + recipientStatuses.forEach(status => { + if (status.patient) { + const patientName = status.patient.name || 'Неизвестный получатель'; + if (!statusesByRecipient[patientName]) { + statusesByRecipient[patientName] = []; + } + statusesByRecipient[patientName].push(status); + } + }); - const statusesHtml = sortedStatuses.map(status => ` -
- + -
-
${status.description}
-
${formatDate(status.created_at)}
-
+
+
${status.description}
+
${formatDate(status.created_at)}
- `).join(''); +
+ `; + + // Генерируем HTML для общих статусов + const commonStatusesHtml = commonStatuses + .sort((a, b) => a.id - b.id) + .map(getStatusHtml) + .join(''); + + // Генерируем HTML для статусов получателей + const recipientStatusesHtml = sortedRecipients.map(recipientName => { + const recipientStatuses = statusesByRecipient[recipientName] + .sort((a, b) => a.id - b.id) + .map(getStatusHtml) + .join(''); + + return ` +
+ + ${recipientName} +
+ ${recipientStatuses} + `; + }).join(''); modalWrapper.innerHTML = ` -
-
- История статусов - -
-
- ${statusesHtml} -
- +
+
+ История статусов +
- `; +
+ ${commonStatusesHtml} + ${recipientStatusesHtml} +
+ +
+ `; document.body.appendChild(modalWrapper); @@ -196,80 +233,13 @@ document.addEventListener('keydown', escHandler); } - // Функция создания модального окна подтверждения отзыва - function createRevokeConfirmModal(singing) { - return new Promise((resolve) => { - const modalWrapper = document.createElement('div'); - modalWrapper.className = 'el-dialog__wrapper'; - modalWrapper.style.cssText = 'z-index: 2001; position: fixed; top: 0; right: 0; bottom: 0; left: 0; overflow: auto;'; - - const backdrop = document.createElement('div'); - backdrop.className = 'v-modal'; - backdrop.style.cssText = 'z-index: 2001;'; - backdrop.tabIndex = 0; - - modalWrapper.innerHTML = ` -
-
- Подтверждение отзыва - -
-
-
- -
-

Вы действительно хотите отозвать документы из подписания?

-
- - Документы: ${singing.documents.map(d => `№${d.number} ${d.title}`).join(', ')} -
-
-
-
- -
- `; - - document.body.appendChild(modalWrapper); - - const closeModal = () => { - modalWrapper.remove(); - resolve(false); - }; - - modalWrapper.querySelector('.el-dialog__headerbtn').addEventListener('click', closeModal); - modalWrapper.querySelector('.cancel-btn').addEventListener('click', closeModal); - modalWrapper.querySelector('.confirm-btn').addEventListener('click', () => { - modalWrapper.remove(); - resolve(true); - }); - modalWrapper.addEventListener('click', (e) => { - if (e.target === modalWrapper) closeModal(); - }); - - // Закрытие по ESC - const escHandler = (e) => { - if (e.key === 'Escape') { - closeModal(); - document.removeEventListener('keydown', escHandler); - } - }; - document.addEventListener('keydown', escHandler); - }); + // Функция для получения имени получателя по ID + function getRecipientNameById(singing, idPatientMis) { + const status = singing.statuses.find(s => s.idPatientMis === idPatientMis && s.patient); + return status?.patient?.name || 'Неизвестный получатель'; } + // Функция отображения PDF async function viewDocument(documentPath, title) { try { @@ -364,6 +334,53 @@ } } + // Функция повторной отправки документов + async function resendDocuments(singing, modalToClose) { + try { + // Сразу закрываем окно со списком документов + if (modalToClose && modalToClose.parentNode) { + modalToClose.remove(); + } + + // Показываем подтверждение с выбором получателей и способа отправки + const result = await createResendConfirmModal(singing); + if (!result.confirmed) return; + + // Если получателей нет или не выбраны, показываем ошибку + if (result.recipients.length === 0) { + showMessage('Не выбраны получатели для повторной отправки', 'error'); + return; + } + + // Если не выбран способ отправки, показываем ошибку + if (!result.deliveryType) { + showMessage('Не выбран способ отправки', 'error'); + return; + } + + showMessage('Повторная отправка документов...', 'info'); + + // Отправляем запрос на повторную отправку с указанием получателей и способа отправки + const response = await sendMessageToContent('resendDocuments', { + trackingId: singing.trackingId, + idPatientMis: result.recipients, + deliveryType: result.deliveryType + }); + + if (response.success) { + showMessage('Документы успешно отправлены повторно', 'success'); + console.log('Документы успешно отправлены повторно'); + // Обновляем список документов + await prepareDocuments(); + } else { + showMessage('Ошибка повторной отправки документов: ' + (response.message || 'Неизвестная ошибка'), 'error'); + } + } catch (error) { + console.error('Ошибка повторной отправки документов:', error); + showMessage('Ошибка повторной отправки документов', 'error'); + } + } + // Функция создания модального окна подтверждения повторной отправки function createResendConfirmModal(singing) { return new Promise((resolve) => { @@ -371,44 +388,259 @@ modalWrapper.className = 'el-dialog__wrapper'; modalWrapper.style.cssText = 'z-index: 2001; position: fixed; top: 0; right: 0; bottom: 0; left: 0; overflow: auto;'; - const backdrop = document.createElement('div'); - backdrop.className = 'v-modal'; - backdrop.style.cssText = 'z-index: 2001;'; - backdrop.tabIndex = 0; + // Собираем всех получателей (recipients + idPatientMis) + const allRecipients = []; + + // Добавляем idPatientMis если есть + if (singing.idPatientMis) { + allRecipients.push({ + id: singing.idPatientMis, + name: getRecipientNameById(singing, singing.idPatientMis) + }); + } + + // Добавляем recipients + if (singing.recipients && singing.recipients.length > 0) { + singing.recipients.forEach(id => { + // Проверяем, не добавили ли уже этого получателя + if (!allRecipients.some(r => r.id === id)) { + allRecipients.push({ + id: id, + name: getRecipientNameById(singing, id) + }); + } + }); + } + + const hasRecipients = allRecipients.length > 0; + + // Маппинг типов доставки на отображаемые названия + const deliveryTypeLabels = { + 'sms': 'СМС-сообщение', + 'max': 'МАХ-сообщение', + 'mila': 'Mila-сообщение', + 'goskey': 'Goskey-сообщение' + }; + + // Доступные типы доставки + const deliveryTypes = Object.keys(deliveryTypeSettings).filter(key => deliveryTypeSettings[key] === true); + + // Создаем HTML для списка получателей с чекбоксами + const recipientsHtml = hasRecipients ? ` +
+
Получатели:
+
+ ${allRecipients.map(recipient => ` +
+ +
+ `).join('')} +
+
+ ` : ''; modalWrapper.innerHTML = ` -
-
- Подтверждение повторной отправки - -
-
-
- -
-

Вы действительно хотите повторно отправить документы на подписание?

-
- - Документы: ${singing.documents.map(d => `№${d.number} ${d.title}`).join(', ')} +
+
+ Подтверждение повторной отправки + +
+
+
+ +
+

Вы действительно хотите повторно отправить документы на подписание?

+
+ + Документы: ${singing.documents.map(d => `№${d.number} ${d.title}`).join(', ')} +
+ ${recipientsHtml} + + +
+
Способ отправки:
+
+ +
+ + ${!hasRecipients ? '

Нет получателей для повторной отправки

' : ''} +
+
+
+ +
+ `; + + document.body.appendChild(modalWrapper); + + // Получаем элементы + const checkboxes = modalWrapper.querySelectorAll('.recipient-checkbox'); + const confirmBtn = modalWrapper.querySelector('.confirm-btn'); + const deliverySelect = modalWrapper.querySelector('.delivery-select'); + let selectedDeliveryType = deliverySelect.value || null; + + // Функция обновления состояния кнопки + function updateConfirmButton() { + const anyChecked = Array.from(checkboxes).some(cb => cb.checked); + const hasDeliveryType = selectedDeliveryType && selectedDeliveryType !== ''; + + if (anyChecked && hasDeliveryType) { + confirmBtn.style.opacity = '1'; + confirmBtn.style.pointerEvents = 'auto'; + } else { + confirmBtn.style.opacity = '0.5'; + confirmBtn.style.pointerEvents = 'none'; + } + } + + // Обработчик изменения выбора в select + deliverySelect.addEventListener('change', (e) => { + selectedDeliveryType = e.target.value; + updateConfirmButton(); + }); + + // Обработчики для чекбоксов + if (checkboxes.length > 0) { + checkboxes.forEach(cb => { + cb.addEventListener('change', updateConfirmButton); + }); + } + + // Функции закрытия + const closeModal = () => { + modalWrapper.remove(); + resolve({ confirmed: false, recipients: [], deliveryType: null }); + }; + + // Обработчик кнопки подтверждения + confirmBtn.addEventListener('click', () => { + const selectedRecipients = Array.from(checkboxes) + .filter(cb => cb.checked) + .map(cb => cb.value); + + if (selectedRecipients.length === 0 || !selectedDeliveryType) { + return; + } + + modalWrapper.remove(); + resolve({ + confirmed: true, + recipients: selectedRecipients, + deliveryType: selectedDeliveryType + }); + }); + + // Обработчики закрытия + modalWrapper.querySelector('.el-dialog__headerbtn').addEventListener('click', closeModal); + modalWrapper.querySelector('.cancel-btn').addEventListener('click', closeModal); + modalWrapper.addEventListener('click', (e) => { + if (e.target === modalWrapper) closeModal(); + }); + + // Закрытие по ESC + const escHandler = (e) => { + if (e.key === 'Escape') { + closeModal(); + document.removeEventListener('keydown', escHandler); + } + }; + document.addEventListener('keydown', escHandler); + + // Инициализация состояния кнопки + updateConfirmButton(); + }); + } + + // Функция создания модального окна подтверждения отзыва + function createRevokeConfirmModal(singing) { + return new Promise((resolve) => { + const modalWrapper = document.createElement('div'); + modalWrapper.className = 'el-dialog__wrapper'; + modalWrapper.style.cssText = 'z-index: 2001; position: fixed; top: 0; right: 0; bottom: 0; left: 0; overflow: auto;'; + + modalWrapper.innerHTML = ` +
+
+ Подтверждение отзыва + +
+
+
+ +
+

Вы действительно хотите отозвать документы из подписания?

+
+ + Документы: ${singing.documents.map(d => `№${d.number} ${d.title}`).join(', ')} +
-
- `; + +
+ `; document.body.appendChild(modalWrapper); @@ -438,43 +670,9 @@ }); } - // Функция повторной отправки документов - async function resendDocuments(singing, modalToClose) { - try { - // Сразу закрываем окно со списком документов - if (modalToClose && modalToClose.parentNode) { - modalToClose.remove(); - } - - // Показываем подтверждение - const confirmed = await createResendConfirmModal(singing); - if (!confirmed) return; - - showMessage('Повторная отправка документов...', 'info'); - - // Отправляем запрос на повторную отправку - const response = await sendMessageToContent('resendDocuments', { - id: singing.id, - trackingId: singing.trackingId, - idPatientMis: singing.idPatientMis - }); - - if (response.success) { - showMessage('Документы успешно отправлены повторно', 'success'); - console.log('Документы успешно отправлены повторно'); - // Обновляем список документов - await prepareDocuments(); - } else { - showMessage('Ошибка повторной отправки документов: ' + (response.message || 'Неизвестная ошибка'), 'error'); - } - } catch (error) { - console.error('Ошибка повторной отправки документов:', error); - showMessage('Ошибка повторной отправки документов', 'error'); - } - } - // Функция создания таблицы документов function createDocumentsTable(data) { + // Создаем модальное окно const modalWrapper = document.createElement('div'); modalWrapper.className = 'el-dialog__wrapper'; @@ -509,9 +707,10 @@ } else { const tableRows = data.map((singing, index) => { // Определяем последний статус - const lastStatus = singing.statuses.reduce((max, status) => + const commonStatuses = singing.statuses.filter(s => s.idPatientMis === null); + const lastStatus = commonStatuses.reduce((max, status) => status.id > max.id ? status : max - , singing.statuses[0]); + , commonStatuses[0] || singing.statuses[0]); const statusClass = getStatusClass(lastStatus.category); const statusIcon = getStatusIcon(lastStatus.category); @@ -519,6 +718,14 @@ lastStatus.category === 'processing' ? '#409EFF' : lastStatus.category === 'error' ? '#F56C6C' : '#909399'; + // Маппинг типов доставки на отображаемые названия + const deliveryTypeLabels = { + 'sms': 'СМС-сообщение', + 'max': 'МАХ-сообщение', + 'mila': 'Mila-сообщение', + 'goskey': 'Goskey-сообщение' + }; + // Формируем ячейку с документами const documentsHtml = singing.documents.map(doc => `
@@ -550,47 +757,47 @@ ${documentsHtml} -
-
+
+
${lastStatus.description}
-
${formatDate(lastStatus.created_at)}
+
${formatDate(lastStatus.created_at)} ✉️ ${deliveryTypeLabels[singing.deliveryType]}
-
- ${singing.storagePath ? ` - - ` : ''} +
+ ${singing.storagePath && singing.storagePath.length > 0 + ? singing.storagePath.map((path, index) => ` + + `).join('') + : ''} ${singing.statuses.length > 1 ? ` ` : ''} ${lastStatus.category === 'processing' ? ` -
- - -
+ + ` : ''}
@@ -707,7 +914,8 @@ const response = await sendMessageToContent('prepareDocuments', {}); if (response.success) { - createDocumentsTable(response.data); + deliveryTypeSettings = response.data.deliveryType; + createDocumentsTable(response.data.singings); } else { showMessage('Ошибка загрузки документов: ' + (response.message || 'Неизвестная ошибка'), 'error'); } diff --git a/metaData.js b/metaData.js index c4ea1d6..4612941 100644 --- a/metaData.js +++ b/metaData.js @@ -37,7 +37,7 @@ patientPhone = window.gon.specific.client.phone; const patientPhoneNormalized = `+7${patientPhone.replace(/[^\d]/g, '').slice(1, 12)}`; - metaData.patients = [{ + metaData.patient = { idPatientMis: window.gon.specific.client.id, familyName: window.gon.specific.client.surname, givenName: window.gon.specific.client.name, @@ -55,10 +55,10 @@ documentName: 'СНИЛС', idDocumentType: 223 }] - }] + } if (window.gon.specific.client.email) { - metaData.patients[0].telecom.push({ + metaData.patient.telecom.push({ system: 'Email', value: window.gon.specific.client.email }); @@ -73,11 +73,6 @@ data: doc.data })); - metaData.userData = { - id: window.gon.application.current_user.id, - login: window.gon.application.current_user.username, - }; - // Отправляем только один раз window.postMessage( { diff --git a/popup.html b/popup.html index 1c0c1b5..bf506af 100644 --- a/popup.html +++ b/popup.html @@ -612,7 +612,6 @@ word-wrap: break-word; overflow-wrap: break-word; hyphens: auto; - cursor: help; position: relative; max-width: 120px; } diff --git a/popup.js b/popup.js index e9911a7..cb6bb0d 100644 --- a/popup.js +++ b/popup.js @@ -68,9 +68,9 @@ function getStatusIcon(category) { switch (category) { - case 'completed': return '✓'; - case 'processing': return '⋯'; - case 'error': return '✗'; + case 'completed': return '✅'; + case 'processing': return '⌛'; + case 'error': return '❌'; default: return '?'; } } @@ -312,6 +312,7 @@ } } + // Рендер таблицы // Рендер таблицы function renderTable(data) { if (!data || data.length === 0) { @@ -323,56 +324,78 @@ emptyState.classList.add('hidden'); const rows = data.map(item => { - const lastStatus = item.statuses?.reduce((max, s) => s.id > max.id ? s : max, item.statuses[0]); - const statusClass = getStatusClass(lastStatus?.category); - const statusIcon = getStatusIcon(lastStatus?.category); + // Берем только общие статусы (где idPatientMis === null) + const commonStatuses = item.statuses?.filter(s => s.idPatientMis === null) || []; + + // Берем последний общий статус + const lastStatus = commonStatuses.length > 0 + ? commonStatuses.reduce((max, s) => s.id > max.id ? s : max, commonStatuses[0]) + : null; + + // Если нет общих статусов, используем первый статус из списка как fallback + const displayStatus = lastStatus || (item.statuses && item.statuses[0]) || null; + + const statusClass = getStatusClass(displayStatus?.category); + const statusIcon = getStatusIcon(displayStatus?.category); + + // Маппинг типов доставки на отображаемые названия + const deliveryTypeLabels = { + 'sms': 'СМС', + 'max': 'МАХ', + 'mila': 'Mila', + 'goskey': 'Goskey' + }; const documentsHtml = item.documents?.map(doc => { - const isClickable = lastStatus?.category !== 'error'; + // Документы можно просматривать только если статус не error + const isClickable = displayStatus?.category !== 'error' && doc.storagePath; const baseClass = isClickable ? 'doc-item clickable-doc' : 'doc-item'; return ` -
- №${doc.number} ${doc.title?.replace(/\s+/g, ' ')} - ${isClickable ? '📄' : ''} -
- `; +
+ №${doc.number} ${doc.title?.replace(/\s+/g, ' ')} + ${isClickable ? '📄' : ''} +
+ `; }).join(''); - const hasMainDocument = item.storagePath && !item.storagePath.includes('null'); + const hasMainDocument = item.storagePath.length > 0; return ` - - -
${item.patientName || 'Неизвестно'}
-
${formatDate(item.created_at)}
-
- 👤 ${item.userName || 'Неизвестно'} + + +
${item.patientName || 'Неизвестно'}
+
${formatDate(item.created_at)}
+
✉️ ${deliveryTypeLabels[item.deliveryType] || item.deliveryType}
+
+ 👤 ${item.userName || 'Неизвестно'} +
+ + +
+ ${documentsHtml} +
+ + +
+
+ ${statusIcon} + ${displayStatus?.name || 'Неизвестно'}
- - -
- ${documentsHtml} -
- - -
-
- ${statusIcon} - ${lastStatus?.name || 'Неизвестно'} -
-
${formatDate(lastStatus?.created_at || item.created_at)}
- ${hasMainDocument ? ` - - ` : ''} -
- - + `).join('') + : ''} +
+ + `; }).join(''); diff --git a/search.html b/search.html index 66c8d50..30e07d6 100644 --- a/search.html +++ b/search.html @@ -94,7 +94,7 @@
-
+
+ +
+ + +
+ -
+
@@ -194,7 +206,7 @@ - + diff --git a/search.js b/search.js index 74fd7b8..562ea6c 100644 --- a/search.js +++ b/search.js @@ -8,6 +8,7 @@ dateFrom: null, dateTo: null, status: 'all', + deliveryType: 'all', signatureType: 'all', senderName: '', patientName: '', @@ -30,27 +31,6 @@ // Bootstrap модальное окно let statusModal = null; - // Статусы из background.js - const statuses = { - 201: { name: "Создан", description: "Документ создан через API, готов к обработке", category: "processing" }, - 202: { name: "Отправлен на подписание", description: "Документ отправлен получателю", category: "processing" }, - 203: { name: "Просмотрел", description: "Документ просмотрен получателем", category: "processing" }, - 204: { name: "Подписал", description: "Документ подписан получателем", category: "completed" }, - 205: { name: "Отказался", description: "Получатель отказался от подписания", category: "error" }, - 206: { name: "Срок для подписания истек", description: "Срок действия ссылки истек до момента проставления подписи или отказа", category: "error" }, - 207: { name: "Ожидание действий всех получателей", description: "Ожидание действий от всех получателей", category: "processing" }, - 208: { name: "Успешно. Подписан всеми — обработка", description: "Все получатели подписали, документ обрабатывается", category: "processing" }, - 209: { name: "Успешно. Подписан не всеми — обработка", description: "Не все получатели подписали, документ обрабатывается", category: "processing" }, - 210: { name: "Завершено. Подписано всеми", description: "Все получатели подписали, документ сохранен", category: "completed" }, - 211: { name: "Завершено. Подписано не всеми", description: "Не все получатели подписали, документ сохранен", category: "completed" }, - 212: { name: "Отменен для получателя", description: "Подписание отменено для конкретного получателя", category: "error" }, - 213: { name: "Завершено. Отменено для всех", description: "Подписание отменено для всех получателей", category: "completed" }, - 498: { name: "Завершено. Отклонено всеми", description: "Все получатели отказались от подписания", category: "error" }, - 499: { name: "Завершено. Истекло для всех", description: "Срок ссылки истек для всех получателей", category: "error" }, - 500: { name: "Ошибка обработки", description: "Ошибка во время обработки документа", category: "error" }, - 501: { name: "Ошибка доставки получателю", description: "Произошла ошибка при доставке конкретному получателю", category: "error" } - }; - // DOM элементы const elements = { periodSelect: document.getElementById('periodSelect'), @@ -58,6 +38,7 @@ dateFrom: document.getElementById('dateFrom'), dateTo: document.getElementById('dateTo'), statusSelect: document.getElementById('statusSelect'), + deliveryTypeSelect: document.getElementById('deliveryTypeSelect'), signatureTypeSelect: document.getElementById('signatureTypeSelect'), senderInput: document.getElementById('senderInput'), patientInput: document.getElementById('patientInput'), @@ -118,6 +99,7 @@ // Применяем фильтры к элементам управления elements.periodSelect.value = state.filters.period; elements.statusSelect.value = state.filters.status; + elements.deliveryTypeSelect.value = state.filters.deliveryType; elements.signatureTypeSelect.value = state.filters.signatureType; elements.senderInput.value = state.filters.senderName || ''; elements.patientInput.value = state.filters.patientName || ''; @@ -148,6 +130,7 @@ // Фильтры elements.statusSelect.addEventListener('change', updateFilters); + elements.deliveryTypeSelect.addEventListener('change', updateFilters); elements.signatureTypeSelect.addEventListener('change', updateFilters); elements.senderInput.addEventListener('input', debounce(updateFilters, 500)); elements.patientInput.addEventListener('input', debounce(updateFilters, 500)); @@ -216,18 +199,17 @@ switch (period) { case 'today': break; - case 'yesterday': - from.setDate(from.getDate() - 1); - today.setDate(today.getDate() - 1); + case '3days': + from.setDate(from.getDate() - 3); break; - case 'week': + case '7days': from.setDate(from.getDate() - 7); break; - case 'month': - from.setMonth(from.getMonth() - 1); + case '30days': + from.setDate(from.getDate() - 30); break; - case 'quarter': - from.setMonth(from.getMonth() - 3); + case '90days': + from.setDate(from.getDate() - 90); break; } @@ -245,6 +227,7 @@ dateFrom: elements.dateFrom.value, dateTo: elements.dateTo.value, status: elements.statusSelect.value, + deliveryType: elements.deliveryTypeSelect.value, signatureType: elements.signatureTypeSelect.value, senderName: elements.senderInput.value.trim(), patientName: elements.patientInput.value.trim(), @@ -280,6 +263,7 @@ elements.periodSelect.value = 'today'; elements.customDateRange.style.display = 'none'; elements.statusSelect.value = 'all'; + elements.deliveryTypeSelect.value = 'all'; elements.signatureTypeSelect.value = 'all'; elements.senderInput.value = ''; elements.patientInput.value = ''; @@ -311,8 +295,7 @@ }); if (response.success) { - // Обогащаем статусы - state.results = enrichStatuses(response.data || []); + state.results = response.data || []; state.pagination.totalItems = state.results.length; state.pagination.totalPages = Math.ceil(state.pagination.totalItems / state.pagination.itemsPerPage); state.pagination.currentPage = 1; @@ -329,31 +312,6 @@ } } - // Обогащение статусов - function enrichStatuses(data) { - if (!Array.isArray(data)) return data; - - data.forEach(item => { - if (Array.isArray(item.statuses)) { - item.statuses.forEach(status => { - const statusInfo = statuses[status.status] || { - name: `Статус ${status.status}`, - description: `Неизвестный статус ${status.status}`, - category: "error" - }; - status.name = statusInfo.name; - status.description = statusInfo.description; - status.category = statusInfo.category; - }); - - // Сортируем статусы по id для хронологии - item.statuses.sort((a, b) => a.id - b.id); - } - }); - - return data; - } - // Определение типа подписания function getSignatureType(item) { return item.esiaAuth === true ? 'esia' : 'mchd'; @@ -440,65 +398,116 @@ // Создание строки таблицы function createTableRow(item) { - const lastStatus = item.statuses?.length > 0 ? item.statuses[item.statuses.length - 1] : null; - const statusClass = lastStatus?.category || 'warning'; + // Берем только общие статусы (где idPatientMis === null) + const commonStatuses = item.statuses?.filter(s => s.idPatientMis === null) || []; + + let lastCommonStatus = null; + if (commonStatuses.length > 0) { + lastCommonStatus = commonStatuses.reduce((last, current) => current.id > last.id ? current : last); + } + + // Если нет общих статусов, используем первый статус из списка как fallback + const displayStatus = lastCommonStatus || (item.statuses && item.statuses[0]) || null; + const date = formatDateTime(item.created_at); - // Документы + // Маппинг типов доставки на отображаемые названия + const deliveryTypeLabels = { + 'sms': 'СМС', + 'max': 'МАХ', + 'mila': 'Mila', + 'goskey': 'Goskey' + }; + const deliveryType = item.deliveryType && deliveryTypeLabels[item.deliveryType] || item.deliveryType || '—'; + + // Получаем уникальных получателей из статусов (исключая самого пациента) + const recipients = new Set(); + if (item.statuses && Array.isArray(item.statuses)) { + item.statuses.forEach(status => { + // Проверяем, что это статус получателя (есть patient) и это не сам пациент + if (status.patient && status.patient.name && + status.idPatientMis !== item.idPatientMis) { + recipients.add(status.patient.name); + } + }); + } + + // Преобразуем Set в массив и сортируем + const recipientsList = Array.from(recipients).sort(); + + // Формируем HTML для получателей + const recipientsHtml = recipientsList.length > 0 ? ` +
+ + ${recipientsList.join(', ')} +
+ ` : ''; + + // Документы - теперь название отображается полностью, без обрезания const documentsHtml = item.documents?.map(doc => { - const canView = lastStatus?.category !== 'completed'; + // Документы можно просматривать только если статус не error + const canView = displayStatus?.category !== 'error' && doc.storagePath && doc.storagePath.length > 0; return ` -
-
- №${doc.number} - ${doc.title} - ${canView ? '' : ''} -
+
+
+ №${doc.number} + ${doc.title} + ${canView ? '' : ''}
- `; +
+ `; }).join('') || ''; // Статус - const statusHtml = lastStatus ? ` -
- - - ${lastStatus.name || 'Неизвестно'} - -
${formatDateTime(lastStatus.created_at)}
-
- ` : ''; + const statusHtml = displayStatus ? ` +
+ + + ${displayStatus.name || displayStatus.description || 'Неизвестно'} + +
${formatDateTime(displayStatus.created_at)}
+
+ ` : ''; - // Тип и кнопка + // Тип подписания const signatureTypeClass = getSignatureTypeClass(item); - const finalAction = lastStatus?.category === 'completed' && item.storagePath ? ` - - ` : ''; + `).join('') + : ''; return ` -
- - - - - - - - `; + + + + + + + + + `; } // Bootstrap классы для статусов @@ -620,30 +629,88 @@ // Показать историю статусов (Bootstrap модалка) function showStatusHistory(statuses) { - const statusItems = statuses.map(status => { - const statusInfo = status || { - name: `Статус ${status.status}`, - description: '', - category: 'warning' - }; + if (!statuses || statuses.length === 0) { + document.getElementById('statusHistoryBody').innerHTML = '

Нет истории статусов

'; + if (statusModal) statusModal.show(); + return; + } + // Разделяем статусы на общие и по получателям + const commonStatuses = statuses.filter(s => s.idPatientMis === null); + const recipientStatuses = statuses.filter(s => s.idPatientMis !== null); + + // Группируем статусы по получателям + const statusesByRecipient = {}; + recipientStatuses.forEach(status => { + if (status.patient) { + const patientName = status.patient.name || 'Неизвестный получатель'; + if (!statusesByRecipient[patientName]) { + statusesByRecipient[patientName] = []; + } + statusesByRecipient[patientName].push(status); + } + }); + + // Сортируем получателей по имени + const sortedRecipients = Object.keys(statusesByRecipient).sort(); + + // Функция для создания HTML статуса + const createStatusHtml = (status) => { return `
- - + +
-
${statusInfo.name}
-
${statusInfo.description || 'Нет описания'}
+
${status.name || status.description || 'Неизвестно'}
+
${status.description || ''}
${formatDateTime(status.created_at)}
`; + }; + + // Генерируем HTML для общих статусов (сортируем по id для хронологии) + const commonStatusesHtml = commonStatuses + .sort((a, b) => a.id - b.id) + .map(createStatusHtml) + .join(''); + + // Генерируем HTML для статусов получателей + const recipientStatusesHtml = sortedRecipients.map(recipientName => { + const recipientStatusesList = statusesByRecipient[recipientName] + .sort((a, b) => a.id - b.id) + .map(createStatusHtml) + .join(''); + + return ` +
+
+ + ${recipientName} +
+
+ ${recipientStatusesList} +
+
+ `; }).join(''); - document.getElementById('statusHistoryBody').innerHTML = statusItems || '

Нет истории статусов

'; + const fullHtml = ` +
+ ${commonStatusesHtml} + ${recipientStatusesHtml ? ` +
+
Статусы получателей
+ ${recipientStatusesHtml} +
+ ` : ''} +
+ `; + + document.getElementById('statusHistoryBody').innerHTML = fullHtml; if (statusModal) { statusModal.show(); @@ -663,8 +730,12 @@ // Создаем тост const toastId = 'toast-' + Date.now(); + const bgClass = type === 'success' ? 'bg-success' : + type === 'danger' ? 'bg-danger' : + type === 'warning' ? 'bg-warning' : 'bg-info'; + const toastHtml = ` -
ДатаОтправлено Пациент Документы Статус
${date}${item.patientName || 'Неизвестно'} -
- ${documentsHtml} -
-
${statusHtml}${item.userName || '—'} - ${getSignatureTypeText(item)} - ${finalAction} -
${date}
✉️ ${deliveryType}
+
+ ${item.patientName || 'Неизвестно'} + ${recipientsHtml} +
+
+
+ ${documentsHtml} +
+
${statusHtml}${item.userName || '—'} + ${getSignatureTypeText(item)} + ${finalAction} +