Files
medods_n3health_extension/search.js
T
2026-02-15 16:59:54 +03:00

704 lines
30 KiB
JavaScript

// search.js - адаптированный под Bootstrap
(function () {
// Состояние приложения
let state = {
filters: {
period: 'today',
dateFrom: null,
dateTo: null,
status: '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;
// Статусы из 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'),
customDateRange: document.getElementById('customDateRange'),
dateFrom: document.getElementById('dateFrom'),
dateTo: document.getElementById('dateTo'),
statusSelect: document.getElementById('statusSelect'),
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.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.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 'yesterday':
from.setDate(from.getDate() - 1);
today.setDate(today.getDate() - 1);
break;
case 'week':
from.setDate(from.getDate() - 7);
break;
case 'month':
from.setMonth(from.getMonth() - 1);
break;
case 'quarter':
from.setMonth(from.getMonth() - 3);
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,
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.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 = enrichStatuses(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 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';
}
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) {
const lastStatus = item.statuses?.length > 0 ? item.statuses[item.statuses.length - 1] : null;
const statusClass = lastStatus?.category || 'warning';
const date = formatDateTime(item.created_at);
// Документы
const documentsHtml = item.documents?.map(doc => {
const canView = lastStatus?.category !== 'completed';
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>
`;
}).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 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="Итоговый документ">
<i class="bi bi-file-pdf"></i>
</button>
` : '';
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>
`;
}
// 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) {
const statusItems = statuses.map(status => {
const statusInfo = status || {
name: `Статус ${status.status}`,
description: '',
category: 'warning'
};
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>
</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="small text-muted">${formatDateTime(status.created_at)}</div>
</div>
</div>
`;
}).join('');
document.getElementById('statusHistoryBody').innerHTML = statusItems || '<p class="text-muted text-center">Нет истории статусов</p>';
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 toastHtml = `
<div id="${toastId}" class="toast align-items-center text-white bg-${type} 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: 5000 });
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);
})();