775 lines
31 KiB
JavaScript
775 lines
31 KiB
JavaScript
// search.js - адаптированный под Bootstrap
|
|
|
|
(function () {
|
|
// Состояние приложения
|
|
let state = {
|
|
filters: {
|
|
period: 'today',
|
|
dateFrom: null,
|
|
dateTo: null,
|
|
status: 'all',
|
|
deliveryType: 'all',
|
|
signatureType: 'all',
|
|
senderName: '',
|
|
patientName: '',
|
|
documentNumber: ''
|
|
},
|
|
results: [],
|
|
pagination: {
|
|
currentPage: 1,
|
|
totalPages: 1,
|
|
totalItems: 0,
|
|
itemsPerPage: 10
|
|
},
|
|
loading: false,
|
|
serverConfig: {
|
|
ip: null,
|
|
port: null
|
|
}
|
|
};
|
|
|
|
// Bootstrap модальное окно
|
|
let statusModal = null;
|
|
|
|
// DOM элементы
|
|
const elements = {
|
|
periodSelect: document.getElementById('periodSelect'),
|
|
customDateRange: document.getElementById('customDateRange'),
|
|
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'),
|
|
documentNumberInput: document.getElementById('documentNumberInput'),
|
|
advancedToggle: document.getElementById('advancedToggle'),
|
|
advancedFilters: document.getElementById('advancedFilters'),
|
|
advancedIcon: document.getElementById('advancedIcon'),
|
|
searchBtn: document.getElementById('searchBtn'),
|
|
clearFiltersBtn: document.getElementById('clearFiltersBtn'),
|
|
tableBody: document.getElementById('tableBody'),
|
|
emptyState: document.getElementById('emptyState'),
|
|
resultsStats: document.getElementById('resultsStats'),
|
|
pagination: document.getElementById('pagination'),
|
|
paginationInfo: document.getElementById('paginationInfo'),
|
|
prevPage: document.getElementById('prevPage'),
|
|
nextPage: document.getElementById('nextPage')
|
|
};
|
|
|
|
// Инициализация
|
|
async function init() {
|
|
// Инициализация Bootstrap модального окна
|
|
const modalElement = document.getElementById('statusHistoryModal');
|
|
if (modalElement) {
|
|
statusModal = new bootstrap.Modal(modalElement);
|
|
}
|
|
|
|
await loadServerConfig();
|
|
await loadFiltersFromStorage();
|
|
setupEventListeners();
|
|
setDefaultDates();
|
|
|
|
// Автоматически выполняем поиск при загрузке
|
|
setTimeout(() => performSearch(), 100);
|
|
}
|
|
|
|
// Загрузка конфигурации сервера
|
|
async function loadServerConfig() {
|
|
return new Promise((resolve) => {
|
|
chrome.storage.local.get(['serverIp', 'serverPort'], (result) => {
|
|
if (result.serverIp && result.serverPort) {
|
|
state.serverConfig.ip = result.serverIp;
|
|
state.serverConfig.port = result.serverPort;
|
|
} else {
|
|
showAlert('Сервер не настроен. Перейдите в настройки расширения.', 'warning');
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
// Загрузка фильтров из storage
|
|
async function loadFiltersFromStorage() {
|
|
return new Promise((resolve) => {
|
|
chrome.storage.local.get(['advancedSearchFilters'], (result) => {
|
|
if (result.advancedSearchFilters) {
|
|
state.filters = { ...state.filters, ...result.advancedSearchFilters };
|
|
|
|
// Применяем фильтры к элементам управления
|
|
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 || '';
|
|
elements.documentNumberInput.value = state.filters.documentNumber || '';
|
|
|
|
if (state.filters.dateFrom) elements.dateFrom.value = state.filters.dateFrom;
|
|
if (state.filters.dateTo) elements.dateTo.value = state.filters.dateTo;
|
|
|
|
// Если период произвольный - показываем поля дат
|
|
if (state.filters.period === 'custom') {
|
|
elements.customDateRange.style.display = 'block';
|
|
}
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
// Сохранение фильтров в storage
|
|
function saveFiltersToStorage() {
|
|
chrome.storage.local.set({ advancedSearchFilters: state.filters });
|
|
}
|
|
|
|
// Установка обработчиков событий
|
|
function setupEventListeners() {
|
|
// Период
|
|
elements.periodSelect.addEventListener('change', handlePeriodChange);
|
|
|
|
// Фильтры
|
|
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));
|
|
elements.documentNumberInput.addEventListener('input', debounce(updateFilters, 500));
|
|
|
|
// Произвольные даты
|
|
elements.dateFrom.addEventListener('change', updateFilters);
|
|
elements.dateTo.addEventListener('change', updateFilters);
|
|
|
|
// Кнопки
|
|
elements.searchBtn.addEventListener('click', performSearch);
|
|
elements.clearFiltersBtn.addEventListener('click', clearFilters);
|
|
|
|
// Расширенные фильтры
|
|
elements.advancedToggle.addEventListener('click', toggleAdvancedFilters);
|
|
|
|
// Пагинация
|
|
elements.prevPage.addEventListener('click', () => changePage(-1));
|
|
elements.nextPage.addEventListener('click', () => changePage(1));
|
|
}
|
|
|
|
// Установка дат по умолчанию
|
|
function setDefaultDates() {
|
|
if (!state.filters.dateFrom || !state.filters.dateTo) {
|
|
const today = new Date();
|
|
const yesterday = new Date(today);
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
|
|
elements.dateTo.value = formatDate(today);
|
|
elements.dateFrom.value = formatDate(yesterday);
|
|
|
|
state.filters.dateFrom = elements.dateFrom.value;
|
|
state.filters.dateTo = elements.dateTo.value;
|
|
}
|
|
}
|
|
|
|
// Форматирование даты для input
|
|
function formatDate(date) {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
// Обработчик изменения периода
|
|
function handlePeriodChange() {
|
|
const period = elements.periodSelect.value;
|
|
|
|
if (period === 'custom') {
|
|
elements.customDateRange.style.display = 'block';
|
|
elements.advancedFilters.style.display = 'block';
|
|
elements.advancedIcon.className = 'bi bi-chevron-up me-1';
|
|
} else {
|
|
elements.customDateRange.style.display = 'none';
|
|
updateDateRangeFromPeriod(period);
|
|
}
|
|
|
|
updateFilters();
|
|
}
|
|
|
|
// Обновление диапазона дат на основе периода
|
|
function updateDateRangeFromPeriod(period) {
|
|
const today = new Date();
|
|
let from = new Date(today);
|
|
|
|
switch (period) {
|
|
case 'today':
|
|
break;
|
|
case '3days':
|
|
from.setDate(from.getDate() - 3);
|
|
break;
|
|
case '7days':
|
|
from.setDate(from.getDate() - 7);
|
|
break;
|
|
case '30days':
|
|
from.setDate(from.getDate() - 30);
|
|
break;
|
|
case '90days':
|
|
from.setDate(from.getDate() - 90);
|
|
break;
|
|
}
|
|
|
|
elements.dateFrom.value = formatDate(from);
|
|
elements.dateTo.value = formatDate(today);
|
|
|
|
state.filters.dateFrom = elements.dateFrom.value;
|
|
state.filters.dateTo = elements.dateTo.value;
|
|
}
|
|
|
|
// Обновление состояния фильтров
|
|
function updateFilters() {
|
|
state.filters = {
|
|
period: elements.periodSelect.value,
|
|
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(),
|
|
documentNumber: elements.documentNumberInput.value.trim()
|
|
};
|
|
|
|
saveFiltersToStorage();
|
|
}
|
|
|
|
// Переключение расширенных фильтров
|
|
function toggleAdvancedFilters() {
|
|
const isVisible = elements.advancedFilters.style.display !== 'none';
|
|
|
|
if (isVisible) {
|
|
elements.advancedFilters.style.display = 'none';
|
|
elements.advancedIcon.className = 'bi bi-chevron-down me-1';
|
|
|
|
if (state.filters.period !== 'custom') {
|
|
elements.customDateRange.style.display = 'none';
|
|
}
|
|
} else {
|
|
elements.advancedFilters.style.display = 'block';
|
|
elements.advancedIcon.className = 'bi bi-chevron-up me-1';
|
|
|
|
if (state.filters.period === 'custom') {
|
|
elements.customDateRange.style.display = 'block';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Сброс фильтров
|
|
function clearFilters() {
|
|
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 = '';
|
|
elements.documentNumberInput.value = '';
|
|
|
|
setDefaultDates();
|
|
updateFilters();
|
|
|
|
elements.advancedFilters.style.display = 'none';
|
|
elements.advancedIcon.className = 'bi bi-chevron-down me-1';
|
|
|
|
// Выполняем поиск после сброса
|
|
performSearch();
|
|
}
|
|
|
|
// Выполнение поиска
|
|
async function performSearch() {
|
|
if (!validateServerConfig()) {
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
updateFilters();
|
|
|
|
const response = await sendMessageToBackground('advancedSearch', {
|
|
filters: state.filters
|
|
});
|
|
|
|
if (response.success) {
|
|
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;
|
|
|
|
renderResults();
|
|
} else {
|
|
showAlert(response.message || 'Ошибка при поиске', 'danger');
|
|
}
|
|
} catch (error) {
|
|
console.error('Search error:', error);
|
|
showAlert('Ошибка при выполнении поиска', 'danger');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
// Определение типа подписания
|
|
function getSignatureType(item) {
|
|
return item.esiaAuth === true ? 'esia' : 'mchd';
|
|
}
|
|
|
|
function getSignatureTypeText(item) {
|
|
return getSignatureType(item) === 'esia' ? 'УКЭП для ЕСИА' : 'МЧД для других';
|
|
}
|
|
|
|
function getSignatureTypeClass(item) {
|
|
return getSignatureType(item) === 'esia' ? 'bg-primary-subtle text-primary' : 'bg-secondary-subtle text-secondary';
|
|
}
|
|
|
|
// Отправка сообщения в background
|
|
function sendMessageToBackground(action, data) {
|
|
return new Promise((resolve) => {
|
|
chrome.runtime.sendMessage({ action, data }, (response) => {
|
|
resolve(response || { success: false, message: 'Нет ответа от background' });
|
|
});
|
|
});
|
|
}
|
|
|
|
// Валидация конфигурации сервера
|
|
function validateServerConfig() {
|
|
if (!state.serverConfig.ip || !state.serverConfig.port) {
|
|
showAlert('Сервер не настроен. Перейдите в настройки расширения.', 'warning');
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Установка состояния загрузки
|
|
function setLoading(isLoading) {
|
|
state.loading = isLoading;
|
|
|
|
if (isLoading) {
|
|
elements.searchBtn.disabled = true;
|
|
elements.tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" class="text-center py-5">
|
|
<div class="spinner-border text-primary mb-3" role="status">
|
|
<span class="visually-hidden">Загрузка...</span>
|
|
</div>
|
|
<div class="text-muted">Поиск документов...</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
elements.emptyState.style.display = 'none';
|
|
} else {
|
|
elements.searchBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// Отображение результатов
|
|
function renderResults() {
|
|
if (state.results.length === 0) {
|
|
elements.tableBody.innerHTML = '';
|
|
elements.emptyState.style.display = 'block';
|
|
elements.pagination.style.display = 'none';
|
|
elements.resultsStats.textContent = 'Найдено: 0';
|
|
return;
|
|
}
|
|
|
|
elements.emptyState.style.display = 'none';
|
|
elements.pagination.style.display = 'flex';
|
|
|
|
// Пагинация
|
|
const startIndex = (state.pagination.currentPage - 1) * state.pagination.itemsPerPage;
|
|
const endIndex = Math.min(startIndex + state.pagination.itemsPerPage, state.results.length);
|
|
const paginatedResults = state.results.slice(startIndex, endIndex);
|
|
|
|
const rows = paginatedResults.map(item => createTableRow(item)).join('');
|
|
elements.tableBody.innerHTML = rows;
|
|
|
|
// Обновляем статистику
|
|
elements.resultsStats.textContent = `Найдено: ${state.pagination.totalItems}`;
|
|
|
|
// Обновляем пагинацию
|
|
updatePagination();
|
|
|
|
// Добавляем обработчики
|
|
addTableEventListeners();
|
|
}
|
|
|
|
// Создание строки таблицы
|
|
function createTableRow(item) {
|
|
// Берем только общие статусы (где 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 => {
|
|
// Документы можно просматривать только если статус не 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 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 = 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 = 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}<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 классы для статусов
|
|
function getStatusBadgeClass(category) {
|
|
switch (category) {
|
|
case 'completed': return 'bg-success-subtle text-success';
|
|
case 'processing': return 'bg-info-subtle text-info';
|
|
case 'error': return 'bg-danger-subtle text-danger';
|
|
default: return 'bg-warning-subtle text-warning';
|
|
}
|
|
}
|
|
|
|
// Форматирование даты и времени
|
|
function formatDateTime(dateString) {
|
|
if (!dateString) return '—';
|
|
const date = new Date(dateString.replace(' ', 'T'));
|
|
return date.toLocaleString('ru-RU', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
// Получение иконки статуса
|
|
function getStatusIcon(category) {
|
|
switch (category) {
|
|
case 'completed': return 'bi-check-circle-fill';
|
|
case 'processing': return 'bi-clock-fill';
|
|
case 'error': return 'bi-exclamation-circle-fill';
|
|
default: return 'bi-question-circle-fill';
|
|
}
|
|
}
|
|
|
|
// Обновление пагинации
|
|
function updatePagination() {
|
|
const { currentPage, totalPages, totalItems } = state.pagination;
|
|
|
|
elements.paginationInfo.textContent = `Страница ${currentPage} из ${totalPages || 1}`;
|
|
|
|
elements.prevPage.disabled = currentPage <= 1;
|
|
elements.nextPage.disabled = currentPage >= totalPages;
|
|
|
|
elements.pagination.style.display = totalPages > 1 ? 'flex' : 'none';
|
|
}
|
|
|
|
// Изменение страницы
|
|
function changePage(delta) {
|
|
const newPage = state.pagination.currentPage + delta;
|
|
|
|
if (newPage >= 1 && newPage <= state.pagination.totalPages) {
|
|
state.pagination.currentPage = newPage;
|
|
renderResults();
|
|
}
|
|
}
|
|
|
|
// Добавление обработчиков для таблицы
|
|
function addTableEventListeners() {
|
|
// Кликабельные документы
|
|
document.querySelectorAll('.document-item[data-can-view="true"]').forEach(el => {
|
|
el.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const path = el.dataset.docPath;
|
|
const title = el.dataset.docTitle;
|
|
if (path) viewDocument(path, title);
|
|
});
|
|
});
|
|
|
|
// Кликабельные статусы с историей
|
|
document.querySelectorAll('.status-badge[data-status-history]').forEach(el => {
|
|
el.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const historyData = el.dataset.statusHistory;
|
|
if (historyData) {
|
|
showStatusHistory(JSON.parse(historyData));
|
|
}
|
|
});
|
|
});
|
|
|
|
// Кнопки просмотра итогового документа
|
|
document.querySelectorAll('.view-final-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const path = btn.dataset.path;
|
|
const title = btn.dataset.title;
|
|
if (path) viewDocument(path, title);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Просмотр документа
|
|
async function viewDocument(path, title) {
|
|
try {
|
|
showAlert('Загрузка документа...', 'info');
|
|
|
|
const response = await sendMessageToBackground('getDocument', { documentPath: path });
|
|
|
|
if (response.success && response.data) {
|
|
const byteCharacters = atob(response.data);
|
|
const byteNumbers = new Array(byteCharacters.length);
|
|
for (let i = 0; i < byteCharacters.length; i++) {
|
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
}
|
|
const byteArray = new Uint8Array(byteNumbers);
|
|
const blob = new Blob([byteArray], { type: 'application/pdf' });
|
|
const url = window.URL.createObjectURL(blob);
|
|
|
|
window.open(url, '_blank');
|
|
setTimeout(() => window.URL.revokeObjectURL(url), 1000);
|
|
} else {
|
|
showAlert('Ошибка загрузки документа', 'danger');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error viewing document:', error);
|
|
showAlert('Ошибка просмотра документа', 'danger');
|
|
}
|
|
}
|
|
|
|
// Показать историю статусов (Bootstrap модалка)
|
|
function showStatusHistory(statuses) {
|
|
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(status.category)} p-2">
|
|
<i class="bi ${getStatusIcon(status.category)}"></i>
|
|
</span>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<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('');
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
// Отображение уведомления (Bootstrap Toast)
|
|
function showAlert(message, type = 'info') {
|
|
// Создаем контейнер для тостов, если его нет
|
|
let toastContainer = document.querySelector('.toast-container');
|
|
if (!toastContainer) {
|
|
toastContainer = document.createElement('div');
|
|
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
|
|
toastContainer.style.zIndex = '1100';
|
|
document.body.appendChild(toastContainer);
|
|
}
|
|
|
|
// Создаем тост
|
|
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 ${bgClass} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
|
<div class="d-flex">
|
|
<div class="toast-body">
|
|
${message}
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
|
|
|
const toastElement = document.getElementById(toastId);
|
|
const toast = new bootstrap.Toast(toastElement, { autohide: true, delay: 3000 });
|
|
toast.show();
|
|
|
|
// Удаляем после скрытия
|
|
toastElement.addEventListener('hidden.bs.toast', () => {
|
|
toastElement.remove();
|
|
});
|
|
}
|
|
|
|
// Debounce функция
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
// Запуск
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
})(); |