// 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 = `
Загрузка...
Поиск документов...
`; 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 ? `
${recipientsList.join(', ')}
` : ''; // Документы - теперь название отображается полностью, без обрезания const documentsHtml = item.documents?.map(doc => { // Документы можно просматривать только если статус не error const canView = displayStatus?.category !== 'error' && doc.storagePath && doc.storagePath.length > 0; return `
№${doc.number} ${doc.title} ${canView ? '' : ''}
`; }).join('') || ''; // Статус const statusHtml = displayStatus ? `
${displayStatus.name || displayStatus.description || 'Неизвестно'}
${formatDateTime(displayStatus.created_at)}
` : ''; // Тип подписания const signatureTypeClass = getSignatureTypeClass(item); // Кнопка просмотра итогового документа const finalAction = displayStatus?.category === 'completed' && item.storagePath && item.storagePath.length > 0 ? item.storagePath.map((path, index) => ` `).join('') : ''; return ` ${date}
✉️ ${deliveryType}
${item.patientName || 'Неизвестно'} ${recipientsHtml}
${documentsHtml}
${statusHtml} ${item.userName || '—'} ${getSignatureTypeText(item)} ${finalAction} `; } // 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 = '

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

'; 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 `
${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(''); const fullHtml = `
${commonStatusesHtml} ${recipientStatusesHtml ? `
Статусы получателей
${recipientStatusesHtml}
` : ''}
`; 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 = ` `; 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); })();