release 1.1
This commit is contained in:
@@ -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 ? `
|
||||
<div class="recipients-list small text-muted mt-1" style="font-size: 10px; line-height: 1.3;">
|
||||
<i class="bi bi-people me-1"></i>
|
||||
${recipientsList.join(', ')}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
// Документы - теперь название отображается полностью, без обрезания
|
||||
const documentsHtml = item.documents?.map(doc => {
|
||||
const canView = lastStatus?.category !== 'completed';
|
||||
// Документы можно просматривать только если статус не error
|
||||
const canView = displayStatus?.category !== 'error' && doc.storagePath && doc.storagePath.length > 0;
|
||||
return `
|
||||
<div class="document-item p-2 mb-1 bg-light rounded ${canView ? 'text-primary' : 'text-muted'}"
|
||||
data-doc-path="${doc.storagePath || ''}"
|
||||
data-doc-title="${doc.title}"
|
||||
data-can-view="${canView}"
|
||||
style="cursor: ${canView ? 'pointer' : 'default'};">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="fw-semibold">№${doc.number}</span>
|
||||
<span class="small text-truncate flex-grow-1">${doc.title}</span>
|
||||
${canView ? '<i class="bi bi-eye"></i>' : ''}
|
||||
</div>
|
||||
<div class="document-item p-2 mb-1 bg-light rounded ${canView ? 'text-primary' : 'text-muted'}"
|
||||
data-doc-path="${doc.storagePath || ''}"
|
||||
data-doc-title="${doc.title}"
|
||||
data-can-view="${canView}"
|
||||
style="cursor: ${canView ? 'pointer' : 'default'};">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="fw-semibold flex-shrink-0">№${doc.number}</span>
|
||||
<span class="small flex-grow-1" style="white-space: normal; word-break: break-word;" title="${doc.title}">${doc.title}</span>
|
||||
${canView ? '<i class="bi bi-eye flex-shrink-0"></i>' : ''}
|
||||
</div>
|
||||
`;
|
||||
</div>
|
||||
`;
|
||||
}).join('') || '<span class="text-muted small">—</span>';
|
||||
|
||||
// Статус
|
||||
const statusHtml = lastStatus ? `
|
||||
<div>
|
||||
<span class="badge ${getStatusBadgeClass(lastStatus.category)} status-badge ${item.statuses?.length > 1 ? '' : ''}"
|
||||
data-status-history='${JSON.stringify(item.statuses)}'>
|
||||
<i class="bi ${getStatusIcon(lastStatus.category)} me-1"></i>
|
||||
${lastStatus.name || 'Неизвестно'}
|
||||
</span>
|
||||
<div class="small text-muted mt-1">${formatDateTime(lastStatus.created_at)}</div>
|
||||
</div>
|
||||
` : '<span class="text-muted small">—</span>';
|
||||
const statusHtml = displayStatus ? `
|
||||
<div>
|
||||
<span class="badge ${getStatusBadgeClass(displayStatus.category)} status-badge ${item.statuses?.length > 1 ? '' : ''}"
|
||||
data-status-history='${JSON.stringify(item.statuses)}'>
|
||||
<i class="bi ${getStatusIcon(displayStatus.category)} me-1"></i>
|
||||
${displayStatus.name || displayStatus.description || 'Неизвестно'}
|
||||
</span>
|
||||
<div class="small text-muted mt-1">${formatDateTime(displayStatus.created_at)}</div>
|
||||
</div>
|
||||
` : '<span class="text-muted small">—</span>';
|
||||
|
||||
// Тип и кнопка
|
||||
// Тип подписания
|
||||
const signatureTypeClass = getSignatureTypeClass(item);
|
||||
const finalAction = lastStatus?.category === 'completed' && item.storagePath ? `
|
||||
<button class="btn btn-sm btn-outline-primary mt-1 view-final-btn" data-path="${item.storagePath}" data-title="Итоговый документ">
|
||||
|
||||
// Кнопка просмотра итогового документа
|
||||
const finalAction = displayStatus?.category === 'completed' && item.storagePath && item.storagePath.length > 0
|
||||
? item.storagePath.map((path, index) => `
|
||||
<button class="btn btn-sm btn-outline-primary mt-1 view-final-btn" data-path="${path}" title="Итоговый документ ${index + 1}">
|
||||
<i class="bi bi-file-pdf"></i>
|
||||
</button>
|
||||
` : '';
|
||||
`).join('')
|
||||
: '';
|
||||
|
||||
return `
|
||||
<tr data-id="${item.id || ''}">
|
||||
<td class="align-middle"><span class="small">${date}</span></td>
|
||||
<td class="align-middle"><span class="small">${item.patientName || 'Неизвестно'}</span></td>
|
||||
<td class="align-middle" style="max-width: 350px;">
|
||||
<div class="documents-list" style="max-height: 100px; overflow-y: auto;">
|
||||
${documentsHtml}
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle">${statusHtml}</td>
|
||||
<td class="align-middle"><span class="small">${item.userName || '—'}</span></td>
|
||||
<td class="align-middle">
|
||||
<span class="badge ${signatureTypeClass}">${getSignatureTypeText(item)}</span>
|
||||
${finalAction}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
<tr data-id="${item.id || ''}">
|
||||
<td class="align-middle"><span class="small">${date}<br>✉️ ${deliveryType}</span></td>
|
||||
<td class="align-middle">
|
||||
<div>
|
||||
<span class="small fw-semibold" title="${item.patientName || 'Неизвестно'}">${item.patientName || 'Неизвестно'}</span>
|
||||
${recipientsHtml}
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle" style="max-width: 350px;">
|
||||
<div class="documents-list" style="max-height: 100px; overflow-y: auto;">
|
||||
${documentsHtml}
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle">${statusHtml}</td>
|
||||
<td class="align-middle"><span class="small" title="${item.userName || ''}">${item.userName || '—'}</span></td>
|
||||
<td class="align-middle">
|
||||
<span class="badge ${signatureTypeClass}">${getSignatureTypeText(item)}</span>
|
||||
${finalAction}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
// 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 = '<p class="text-muted text-center">Нет истории статусов</p>';
|
||||
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 `
|
||||
<div class="d-flex gap-3 mb-3 pb-2 border-bottom">
|
||||
<div class="flex-shrink-0">
|
||||
<span class="badge ${getStatusBadgeClass(statusInfo.category)} p-2">
|
||||
<i class="bi ${getStatusIcon(statusInfo.category)}"></i>
|
||||
<span class="badge ${getStatusBadgeClass(status.category)} p-2">
|
||||
<i class="bi ${getStatusIcon(status.category)}"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-semibold">${statusInfo.name}</div>
|
||||
<div class="small text-muted mb-1">${statusInfo.description || 'Нет описания'}</div>
|
||||
<div class="fw-semibold">${status.name || status.description || 'Неизвестно'}</div>
|
||||
<div class="small text-muted mb-1">${status.description || ''}</div>
|
||||
<div class="small text-muted">${formatDateTime(status.created_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
// Генерируем 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 `
|
||||
<div class="mt-3 mb-2">
|
||||
<div class="bg-light p-2 rounded">
|
||||
<i class="bi bi-person-circle me-1 text-primary"></i>
|
||||
<span class="fw-semibold">${recipientName}</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
${recipientStatusesList}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
document.getElementById('statusHistoryBody').innerHTML = statusItems || '<p class="text-muted text-center">Нет истории статусов</p>';
|
||||
const fullHtml = `
|
||||
<div class="status-history">
|
||||
${commonStatusesHtml}
|
||||
${recipientStatusesHtml ? `
|
||||
<div class="mt-3 pt-2 border-top">
|
||||
<h6 class="mb-3">Статусы получателей</h6>
|
||||
${recipientStatusesHtml}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div id="${toastId}" class="toast align-items-center text-white bg-${type} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div id="${toastId}" class="toast align-items-center text-white ${bgClass} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
${message}
|
||||
@@ -677,7 +748,7 @@
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
||||
|
||||
const toastElement = document.getElementById(toastId);
|
||||
const toast = new bootstrap.Toast(toastElement, { autohide: true, delay: 5000 });
|
||||
const toast = new bootstrap.Toast(toastElement, { autohide: true, delay: 3000 });
|
||||
toast.show();
|
||||
|
||||
// Удаляем после скрытия
|
||||
|
||||
Reference in New Issue
Block a user