release
This commit is contained in:
Binary file not shown.
+619
@@ -0,0 +1,619 @@
|
|||||||
|
// background.js
|
||||||
|
|
||||||
|
console.log('✅ Medods N3.Health service worker started');
|
||||||
|
|
||||||
|
let isProcessing = false;
|
||||||
|
// Таблица статусов
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function enrichStatuses(data) {
|
||||||
|
if (!Array.isArray(data)) return data;
|
||||||
|
|
||||||
|
data.forEach(singing => {
|
||||||
|
if (Array.isArray(singing.statuses)) {
|
||||||
|
singing.statuses.forEach(status => {
|
||||||
|
const statusInfo = statuses[status.status] || {
|
||||||
|
name: "Неизвестный статус",
|
||||||
|
description: `Статус ${status.status}`,
|
||||||
|
category: "error"
|
||||||
|
};
|
||||||
|
status.category = statusInfo.category;
|
||||||
|
status.description = statusInfo.description;
|
||||||
|
status.name = statusInfo.name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendDataToServer(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/singing`;
|
||||||
|
console.log('🌐 Отправка данных на сервер:', url);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
signal: AbortSignal.timeout(30000)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`HTTP error: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка отправки на сервер:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processAndSendDocuments(data, sender) {
|
||||||
|
if (isProcessing) {
|
||||||
|
throw new Error('Already processing documents');
|
||||||
|
}
|
||||||
|
isProcessing = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const serverResponse = await sendDataToServer(data);
|
||||||
|
|
||||||
|
const isSuccess = serverResponse && serverResponse.status === 'SUCCESS';
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: isSuccess,
|
||||||
|
message: serverResponse.message || 'Неизвестная ошибка',
|
||||||
|
sentCount: data.docs?.length || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка обработки документов:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Ошибка обработки документов',
|
||||||
|
error: error.toString()
|
||||||
|
};
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
isProcessing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
if (message.action === 'processAndSendDocuments') {
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const result = await processAndSendDocuments(message.data, sender);
|
||||||
|
sendResponse({
|
||||||
|
success: result.success,
|
||||||
|
message: result.message,
|
||||||
|
sentCount: result.sentCount,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Неизвестная ошибка',
|
||||||
|
error: error.toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.action === 'bgLog') {
|
||||||
|
try {
|
||||||
|
console.log(`[EXT][${message.sender}]`, ...message.payload);
|
||||||
|
} catch (e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.action === 'preSingingCheck') {
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const result = await preSingingCheck(message.data);
|
||||||
|
sendResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Неизвестная ошибка',
|
||||||
|
error: error.toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.action === 'getDocuments') {
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const result = await getDocumentsFromServer(message.data);
|
||||||
|
sendResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Ошибка получения документов',
|
||||||
|
error: error.toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.action === 'getDocument') {
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const result = await getDocumentFromServer(message.data);
|
||||||
|
sendResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Ошибка получения документа'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.action === 'revokeDocuments') {
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const result = await revokeDocumentsOnServer(message.data);
|
||||||
|
sendResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Ошибка отзыва документов'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.action === 'resendDocuments') {
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const result = await resendDocumentsOnServer(message.data);
|
||||||
|
sendResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Ошибка повторной отправки документов'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Новый обработчик для получения статусов из popup
|
||||||
|
if (message.action === 'getStatuses') {
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const result = await getStatusesFromServer(message.data);
|
||||||
|
sendResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Ошибка получения статусов',
|
||||||
|
error: error.toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик расширенного поиска
|
||||||
|
if (message.action === 'advancedSearch') {
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const result = await advancedSearch(message.data);
|
||||||
|
sendResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Ошибка расширенного поиска',
|
||||||
|
error: error.toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция расширенного поиска
|
||||||
|
async function advancedSearch(data) {
|
||||||
|
if (isProcessing) {
|
||||||
|
throw new Error('Already processing documents');
|
||||||
|
}
|
||||||
|
isProcessing = true;
|
||||||
|
|
||||||
|
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/advanced-search`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
signal: AbortSignal.timeout(30000)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await response.json();
|
||||||
|
result = result.data || [];
|
||||||
|
|
||||||
|
// Обогащаем статусы если есть данные
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
result = enrichStatuses(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result || []
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Advanced search error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Ошибка расширенного поиска',
|
||||||
|
error: error.toString()
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
isProcessing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preSingingCheck(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/pre_singing`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
signal: AbortSignal.timeout(15000)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`HTTP error: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Ошибка предварительной проверки',
|
||||||
|
error: error.toString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDocumentsFromServer(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/documents`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
idPatientMis: data.idPatientMis
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(30000)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`HTTP error: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.status === 'SUCCESS' && Array.isArray(result.data)) {
|
||||||
|
result.data = enrichStatuses(result.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: result.status === 'SUCCESS',
|
||||||
|
data: result.data || [],
|
||||||
|
message: result.message || 'Документы получены успешно'
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Ошибка получения документов',
|
||||||
|
error: error.toString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Новая функция для получения статусов
|
||||||
|
async function getStatusesFromServer(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/statuses`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data || {}),
|
||||||
|
signal: AbortSignal.timeout(30000)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`HTTP error: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await response.json();
|
||||||
|
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
result = enrichStatuses(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result || []
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Ошибка получения статусов',
|
||||||
|
error: error.toString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.runtime.onInstalled.addListener((details) => {
|
||||||
|
if (details.reason === 'install') {
|
||||||
|
chrome.storage.local.set({
|
||||||
|
esiaAuth: false,
|
||||||
|
trackingIds: [],
|
||||||
|
serverIp: '',
|
||||||
|
serverPort: 8000,
|
||||||
|
processingStatus: 'idle'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getDocumentFromServer(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/document`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
signal: AbortSignal.timeout(30000)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result.data
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function revokeDocumentsOnServer(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/revoke`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
signal: AbortSignal.timeout(30000)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return {
|
||||||
|
success: result.status === 'SUCCESS',
|
||||||
|
message: result.message
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resendDocumentsOnServer(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/resend`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
signal: AbortSignal.timeout(30000)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return {
|
||||||
|
success: result.status === 'SUCCESS',
|
||||||
|
message: result.message
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Background script loaded');
|
||||||
Vendored
+2106
File diff suppressed because it is too large
Load Diff
Vendored
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+6
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
+386
@@ -0,0 +1,386 @@
|
|||||||
|
// content.js
|
||||||
|
console.log('[EXT][content] loaded');
|
||||||
|
|
||||||
|
const dataType = 'metaData';
|
||||||
|
const userDataType = 'userData';
|
||||||
|
let isProcessing = false;
|
||||||
|
let storageData = {};
|
||||||
|
|
||||||
|
function injectScript(file) {
|
||||||
|
console.log('[EXT][content] inject:', file);
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = chrome.runtime.getURL(file) + '?t=' + Date.now();
|
||||||
|
script.type = 'text/javascript';
|
||||||
|
script.async = false;
|
||||||
|
|
||||||
|
(document.head || document.documentElement).appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
if (
|
||||||
|
event.source !== window ||
|
||||||
|
!event.data ||
|
||||||
|
event.data.source !== 'medods-extension' ||
|
||||||
|
event.data.type !== dataType &&
|
||||||
|
event.data.type !== userDataType
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[EXT][content] save ${event.data.type}`);
|
||||||
|
|
||||||
|
const saveData = {
|
||||||
|
[event.data.type]: event.data.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.storage.local.set(saveData);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadPageData() {
|
||||||
|
storageData = await chrome.storage.local.get(dataType);
|
||||||
|
|
||||||
|
if (!storageData || !storageData.metaData || Object.keys(storageData.metaData).length === 0) {
|
||||||
|
console.log(`[EXT][content] no data for ${dataType}, injecting...`);
|
||||||
|
injectScript(`${dataType}.js`);
|
||||||
|
} else {
|
||||||
|
console.log(`[EXT][content] metadata already exists in storage`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDocumentsPage() {
|
||||||
|
return (
|
||||||
|
location.href.includes('/clients/') &&
|
||||||
|
location.href.includes('/documents')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMedodsPage() {
|
||||||
|
return (
|
||||||
|
document.title === 'MEDODS'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик сообщений от singing.js
|
||||||
|
window.addEventListener('message', async (event) => {
|
||||||
|
if (
|
||||||
|
event.source !== window ||
|
||||||
|
!event.data ||
|
||||||
|
event.data.source !== 'medods-extension'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ОБРАБОТЧИК ПРЕДВАРИТЕЛЬНОЙ ПРОВЕРКИ
|
||||||
|
if (event.data.type === 'preSingingCheck') {
|
||||||
|
// Проверка занятости
|
||||||
|
if (isProcessing) {
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'preSingingCheckResponse',
|
||||||
|
payload: {
|
||||||
|
success: false,
|
||||||
|
error: 'Система занята, попробуйте позже'
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем данные из storage
|
||||||
|
if (!storageData.metaData) {
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'preSingingCheckResponse',
|
||||||
|
payload: {
|
||||||
|
success: false,
|
||||||
|
error: 'Данные не загружены'
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подготавливаем данные для отправки
|
||||||
|
const userData = storageData.metaData.userData;
|
||||||
|
const patientId = storageData.metaData.patients?.[0]?.idPatientMis;
|
||||||
|
|
||||||
|
if (!userData || !patientId) {
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'preSingingCheckResponse',
|
||||||
|
payload: {
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось получить данные пользователя или пациента'
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preSingingData = {
|
||||||
|
userId: userData.id,
|
||||||
|
patientId: patientId,
|
||||||
|
docNumbers: event.data.payload.docNumbers
|
||||||
|
};
|
||||||
|
|
||||||
|
// Отправляем запрос в background для предварительной проверки
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'preSingingCheck',
|
||||||
|
data: preSingingData
|
||||||
|
}, (response) => {
|
||||||
|
// Пересылаем результат в singing.js
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'preSingingCheckResponse',
|
||||||
|
payload: response
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ОБРАБОТЧИК ОТПРАВКИ ДОКУМЕНТОВ
|
||||||
|
if (event.data.type === 'sendDocuments') {
|
||||||
|
// Проверка занятости
|
||||||
|
if (isProcessing) {
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'sendDocumentsResponse',
|
||||||
|
payload: {
|
||||||
|
response: {
|
||||||
|
success: false,
|
||||||
|
status: 'processing',
|
||||||
|
error: 'Система занята, попробуйте позже'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isProcessing = true;
|
||||||
|
|
||||||
|
console.log('[EXT][content] Forwarding to background:', {
|
||||||
|
docNumbers: event.data.payload.docNumbers
|
||||||
|
});
|
||||||
|
|
||||||
|
// Немедленный ответ о принятии задачи
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'sendDocumentsResponse',
|
||||||
|
payload: {
|
||||||
|
response: {
|
||||||
|
success: true,
|
||||||
|
message: 'Документы приняты в обработку',
|
||||||
|
status: 'processing'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
|
||||||
|
// Фильтруем документы по номерам
|
||||||
|
const filteredDocs = storageData.metaData.docs.filter(doc =>
|
||||||
|
event.data.payload.docNumbers.includes(parseInt(doc.number, 10))
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`📄 [Content] Найдено документов для отправки: ${filteredDocs.length}`);
|
||||||
|
|
||||||
|
if (filteredDocs.length === 0) {
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'sendDocumentsResponse',
|
||||||
|
payload: {
|
||||||
|
response: {
|
||||||
|
success: false,
|
||||||
|
error: 'No matching documents found'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
isProcessing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Собираем все данные для отправки
|
||||||
|
const sendData = {
|
||||||
|
practitioner: storageData.metaData.practitioner,
|
||||||
|
patients: storageData.metaData.patients,
|
||||||
|
docs: filteredDocs,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Асинхронная обработка в background
|
||||||
|
setTimeout(() => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'processAndSendDocuments',
|
||||||
|
data: sendData
|
||||||
|
}, (response) => {
|
||||||
|
isProcessing = false;
|
||||||
|
console.log('[EXT][content] Response from background:', response);
|
||||||
|
|
||||||
|
// Пересылаем результат в singing.js
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'backgroundProcessingResult',
|
||||||
|
payload: {
|
||||||
|
success: response?.success || false,
|
||||||
|
message: response.message || '',
|
||||||
|
sentCount: response?.sentCount || 0
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// В content.js, в основном обработчике window.addEventListener добавьте:
|
||||||
|
|
||||||
|
// ОБРАБОТЧИК ПОДГОТОВКИ ДОКУМЕНТОВ (используем существующие метаданные)
|
||||||
|
if (event.data.type === 'prepareDocuments') {
|
||||||
|
// Проверяем наличие метаданных
|
||||||
|
if (!storageData.metaData) {
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'prepareDocumentsResponse',
|
||||||
|
payload: {
|
||||||
|
success: false,
|
||||||
|
message: 'Метаданные не загружены'
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем idPatientMis из метаданных
|
||||||
|
const idPatientMis = storageData.metaData.patients?.[0]?.idPatientMis;
|
||||||
|
|
||||||
|
if (!idPatientMis) {
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'prepareDocumentsResponse',
|
||||||
|
payload: {
|
||||||
|
success: false,
|
||||||
|
message: 'Не удалось получить idPatientMis из метаданных'
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем запрос в background для получения документов
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'getDocuments',
|
||||||
|
data: {
|
||||||
|
idPatientMis: idPatientMis
|
||||||
|
}
|
||||||
|
}, (response) => {
|
||||||
|
// Пересылаем результат в documents.js
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'prepareDocumentsResponse',
|
||||||
|
payload: response
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ОБРАБОТЧИК ПОЛУЧЕНИЯ ДОКУМЕНТА
|
||||||
|
if (event.data.type === 'getDocument') {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'getDocument',
|
||||||
|
data: event.data.payload
|
||||||
|
}, (response) => {
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'getDocumentResponse',
|
||||||
|
payload: response
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ОБРАБОТЧИК ПРОВЕРКИ ПРАВ НА ОТЗЫВ
|
||||||
|
if (event.data.type === 'checkRevokePermission') {
|
||||||
|
const hasPermission = String(storageData.metaData?.userData?.id) === String(event.data.payload.userIdLpu);
|
||||||
|
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'checkRevokePermissionResponse',
|
||||||
|
payload: {
|
||||||
|
hasPermission: hasPermission
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ОБРАБОТЧИК ОТЗЫВА ДОКУМЕНТОВ
|
||||||
|
if (event.data.type === 'revokeDocuments') {
|
||||||
|
console.log('[EXT][content] Запрос на отзыв документов:', event.data.payload);
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'revokeDocuments',
|
||||||
|
data: event.data.payload
|
||||||
|
}, (response) => {
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'revokeDocumentsResponse',
|
||||||
|
payload: response
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ОБРАБОТЧИК ПОВТОРНОЙ ОТПРАВКИ ДОКУМЕНТОВ
|
||||||
|
if (event.data.type === 'resendDocuments') {
|
||||||
|
console.log('[EXT][content] Запрос на повторную отправку документов:', event.data.payload);
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'resendDocuments',
|
||||||
|
data: event.data.payload
|
||||||
|
}, (response) => {
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'resendDocumentsResponse',
|
||||||
|
payload: response
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Основной цикл
|
||||||
|
let documentsActive = false;
|
||||||
|
let medodsActive = false;
|
||||||
|
let lastCheck = 0;
|
||||||
|
|
||||||
|
function loop(ts) {
|
||||||
|
if (ts - lastCheck > 200) {
|
||||||
|
lastCheck = ts;
|
||||||
|
|
||||||
|
const now = isDocumentsPage();
|
||||||
|
|
||||||
|
if (now && !documentsActive) {
|
||||||
|
console.log('[EXT][content] ENTER documents');
|
||||||
|
loadPageData()
|
||||||
|
.then(() => {
|
||||||
|
injectScript('documents.js');
|
||||||
|
injectScript('singing.js');
|
||||||
|
documentsActive = true;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('[EXT][content] Error on enter documents:', error);
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!now && documentsActive) {
|
||||||
|
console.log('[EXT][content] LEAVE documents');
|
||||||
|
documentsActive = false;
|
||||||
|
isProcessing = false;
|
||||||
|
chrome.storage.local.remove(dataType)
|
||||||
|
.then(() => { })
|
||||||
|
.catch(error => {
|
||||||
|
console.error('[EXT][content] Error on leave documents:', error);
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const medodsNow = isMedodsPage();
|
||||||
|
|
||||||
|
if (medodsNow && !medodsActive) {
|
||||||
|
medodsActive = true;
|
||||||
|
injectScript('userData.js');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск
|
||||||
|
loop(0);
|
||||||
+766
@@ -0,0 +1,766 @@
|
|||||||
|
// documents.js
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
// Глобальная переменная для хранения текущего уведомления
|
||||||
|
let currentMessageEl = null;
|
||||||
|
let messageTimeout = null;
|
||||||
|
|
||||||
|
// Функция для отображения сообщений
|
||||||
|
function showMessage(message, type = 'success', duration = 3000) {
|
||||||
|
// Удаляем предыдущее уведомление, если оно есть
|
||||||
|
if (currentMessageEl && currentMessageEl.parentNode) {
|
||||||
|
clearTimeout(messageTimeout);
|
||||||
|
document.body.removeChild(currentMessageEl);
|
||||||
|
currentMessageEl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.className = `el-message el-message--${type}`;
|
||||||
|
messageEl.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 9999;
|
||||||
|
min-width: 380px;
|
||||||
|
padding: 15px 15px 15px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
background-color: ${type === 'success' ? '#f0f9eb' :
|
||||||
|
type === 'info' ? '#f4f4f5' : '#fef0f0'};
|
||||||
|
border: 1px solid ${type === 'success' ? '#e1f3d8' :
|
||||||
|
type === 'info' ? '#e9e9eb' : '#fde2e2'};
|
||||||
|
color: ${type === 'success' ? '#67c23a' :
|
||||||
|
type === 'info' ? '#909399' : '#f56c6c'};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const iconClass = type === 'success' ? 'el-icon-success' :
|
||||||
|
type === 'info' ? 'el-icon-info' : 'el-icon-error';
|
||||||
|
const iconColor = type === 'success' ? '#67c23a' :
|
||||||
|
type === 'info' ? '#909399' : '#f56c6c';
|
||||||
|
|
||||||
|
messageEl.innerHTML = `
|
||||||
|
<i class="el-icon ${iconClass}" style="font-size: 16px; color: ${iconColor}; margin-right: 10px;"></i>
|
||||||
|
<span style="font-size: 14px;">${message}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(messageEl);
|
||||||
|
currentMessageEl = messageEl;
|
||||||
|
|
||||||
|
// Скрываем через указанное время
|
||||||
|
messageTimeout = setTimeout(() => {
|
||||||
|
if (currentMessageEl === messageEl && messageEl.parentNode) {
|
||||||
|
document.body.removeChild(messageEl);
|
||||||
|
currentMessageEl = null;
|
||||||
|
}
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция отправки сообщения
|
||||||
|
function sendMessageToContent(type, payload) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
reject(new Error('Таймаут ожидания ответа'));
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
const messageHandler = (event) => {
|
||||||
|
if (
|
||||||
|
event.source !== window ||
|
||||||
|
!event.data ||
|
||||||
|
event.data.source !== 'medods-extension' ||
|
||||||
|
event.data.type !== `${type}Response`
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.removeEventListener('message', messageHandler);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
resolve(event.data.payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', messageHandler);
|
||||||
|
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: type,
|
||||||
|
payload: payload,
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция форматирования даты
|
||||||
|
function formatDate(dateString) {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('ru-RU', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция получения класса для статуса
|
||||||
|
function getStatusClass(category) {
|
||||||
|
switch (category) {
|
||||||
|
case 'completed':
|
||||||
|
return 'status-completed';
|
||||||
|
case 'processing':
|
||||||
|
return 'status-processing';
|
||||||
|
case 'error':
|
||||||
|
return 'status-error';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция получения иконки для статуса
|
||||||
|
function getStatusIcon(category) {
|
||||||
|
switch (category) {
|
||||||
|
case 'completed':
|
||||||
|
return 'el-icon-success';
|
||||||
|
case 'processing':
|
||||||
|
return 'el-icon-loading';
|
||||||
|
case 'error':
|
||||||
|
return 'el-icon-error';
|
||||||
|
default:
|
||||||
|
return 'el-icon-question';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция создания модального окна со статусами
|
||||||
|
function createStatusesModal(singing) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
const sortedStatuses = [...singing.statuses].sort((a, b) => a.id - b.id);
|
||||||
|
|
||||||
|
const statusesHtml = sortedStatuses.map(status => `
|
||||||
|
<div class="status-history-item" style="padding: 12px; border-bottom: 1px solid #ebeef5; display: flex; align-items: center; gap: 12px;">
|
||||||
|
<i class="el-icon ${getStatusIcon(status.category)}" style="font-size: 16px; color: ${status.category === 'completed' ? '#67C23A' :
|
||||||
|
status.category === 'processing' ? '#409EFF' :
|
||||||
|
status.category === 'error' ? '#F56C6C' : '#909399'
|
||||||
|
}; width: 20px;"></i>
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<div style="font-weight: 500; color: #303133;">${status.description}</div>
|
||||||
|
<div style="font-size: 12px; color: #909399; margin-top: 4px;">${formatDate(status.created_at)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
modalWrapper.innerHTML = `
|
||||||
|
<div class="el-dialog" style="width: 500px; margin-top: 15vh;">
|
||||||
|
<div class="el-dialog__header">
|
||||||
|
<span class="el-dialog__title">История статусов</span>
|
||||||
|
<button type="button" class="el-dialog__headerbtn" aria-label="Close">
|
||||||
|
<i class="el-dialog__close el-icon el-icon-close"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="el-dialog__body" style="max-height: 400px; overflow-y: auto; padding: 0;">
|
||||||
|
${statusesHtml}
|
||||||
|
</div>
|
||||||
|
<div class="el-dialog__footer" style="margin-top: 12px; width: 100%; text-align: right;">
|
||||||
|
<button type="button" class="el-button m-button el-button--small is-plain button-with-icon close-btn">
|
||||||
|
<i class="m-icon fa-times button-icon fad" style="font-size: 14px; margin-right: 6px;"></i>
|
||||||
|
<span>Закрыть</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modalWrapper);
|
||||||
|
|
||||||
|
// Обработчики закрытия
|
||||||
|
const closeModal = () => modalWrapper.remove();
|
||||||
|
modalWrapper.querySelector('.el-dialog__headerbtn').addEventListener('click', closeModal);
|
||||||
|
modalWrapper.querySelector('.close-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция создания модального окна подтверждения отзыва
|
||||||
|
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 = `
|
||||||
|
<div class="el-dialog" style="width: 450px; margin-top: 25vh;">
|
||||||
|
<div class="el-dialog__header">
|
||||||
|
<span class="el-dialog__title">Подтверждение отзыва</span>
|
||||||
|
<button type="button" class="el-dialog__headerbtn" aria-label="Close">
|
||||||
|
<i class="el-dialog__close el-icon el-icon-close"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="el-dialog__body" style="padding: 20px;">
|
||||||
|
<div style="display: flex; align-items: flex-start; gap: 12px;">
|
||||||
|
<i class="el-icon el-icon-warning" style="font-size: 24px; color: #E6A23C;"></i>
|
||||||
|
<div>
|
||||||
|
<p style="margin: 0 0 10px 0; color: #606266;">Вы действительно хотите отозвать документы из подписания?</p>
|
||||||
|
<div style="background: #f8f8f8; padding: 10px; border-radius: 4px; font-size: 13px; color: #E6A23C; border-left: 3px solid #E6A23C;">
|
||||||
|
<i class="el-icon el-icon-document" style="margin-right: 6px;"></i>
|
||||||
|
<strong>Документы:</strong> ${singing.documents.map(d => `№${d.number} ${d.title}`).join(', ')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="el-dialog__footer" style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<button type="button" class="el-button m-button el-button--small is-plain button-with-icon cancel-btn"
|
||||||
|
style="display: flex; align-items: center;">
|
||||||
|
<i class="m-icon fa-times button-icon fad" style="font-size: 14px; margin-right: 6px;"></i>
|
||||||
|
<span>Отмена</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="el-button m-button el-button--small is-plain button-with-icon confirm-btn" style="color: #F56C6C; border-color: #F56C6C; display: flex; align-items: center;">
|
||||||
|
<i class="m-icon fa-ban button-icon fad" style="font-size: 14px; margin-right: 6px;"></i>
|
||||||
|
<span>Отозвать</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция отображения PDF
|
||||||
|
async function viewDocument(documentPath, title) {
|
||||||
|
try {
|
||||||
|
showMessage('Загрузка документа...', 'info', 1000);
|
||||||
|
|
||||||
|
const response = await sendMessageToContent('getDocument', { documentPath });
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
// Декодируем base64
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Создаем blob
|
||||||
|
const blob = new Blob([byteArray], { type: 'application/pdf' });
|
||||||
|
|
||||||
|
// Создаем URL для blob
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Пытаемся открыть в новой вкладке
|
||||||
|
const newWindow = window.open(url, '_blank');
|
||||||
|
|
||||||
|
// Если браузер заблокировал всплывающее окно
|
||||||
|
if (!newWindow) {
|
||||||
|
// Создаем ссылку и имитируем клик для скачивания
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = 'document.pdf'; // Простое имя по умолчанию
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
showMessage('Разрешите всплывающие окна для просмотра PDF', 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем URL через некоторое время
|
||||||
|
setTimeout(() => {
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
showMessage('Ошибка загрузки документа: ' + (response.message || 'Неизвестная ошибка'), 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка просмотра документа:', error);
|
||||||
|
showMessage('Ошибка просмотра документа', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция отзыва документов
|
||||||
|
async function revokeDocuments(singing, modalToClose) {
|
||||||
|
try {
|
||||||
|
// Сразу закрываем окно со списком документов
|
||||||
|
if (modalToClose && modalToClose.parentNode) {
|
||||||
|
modalToClose.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем права на отзыв
|
||||||
|
const checkResult = await sendMessageToContent('checkRevokePermission', {
|
||||||
|
userIdLpu: singing.userIdLpu
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!checkResult.hasPermission) {
|
||||||
|
showMessage('Отозвать документы может только автор запроса', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем подтверждение
|
||||||
|
const confirmed = await createRevokeConfirmModal(singing);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
showMessage('Отзыв документов...', 'info');
|
||||||
|
|
||||||
|
// Отправляем запрос на отзыв
|
||||||
|
const response = await sendMessageToContent('revokeDocuments', {
|
||||||
|
trackingId: singing.trackingId
|
||||||
|
});
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
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 = `
|
||||||
|
<div class="el-dialog" style="width: 450px; margin-top: 25vh;">
|
||||||
|
<div class="el-dialog__header">
|
||||||
|
<span class="el-dialog__title">Подтверждение повторной отправки</span>
|
||||||
|
<button type="button" class="el-dialog__headerbtn" aria-label="Close">
|
||||||
|
<i class="el-dialog__close el-icon el-icon-close"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="el-dialog__body" style="padding: 20px;">
|
||||||
|
<div style="display: flex; align-items: flex-start; gap: 12px;">
|
||||||
|
<i class="el-icon el-icon-warning" style="font-size: 24px; color: #E6A23C;"></i>
|
||||||
|
<div>
|
||||||
|
<p style="margin: 0 0 10px 0; color: #606266;">Вы действительно хотите повторно отправить документы на подписание?</p>
|
||||||
|
<div style="background: #f8f8f8; padding: 10px; border-radius: 4px; font-size: 13px; color: #E6A23C; border-left: 3px solid #E6A23C;">
|
||||||
|
<i class="el-icon el-icon-document" style="margin-right: 6px;"></i>
|
||||||
|
<strong>Документы:</strong> ${singing.documents.map(d => `№${d.number} ${d.title}`).join(', ')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="el-dialog__footer" style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<button type="button" class="el-button m-button el-button--small is-plain button-with-icon cancel-btn"
|
||||||
|
style="display: flex; align-items: center;">
|
||||||
|
<i class="m-icon fa-times button-icon fad" style="font-size: 14px; margin-right: 6px;"></i>
|
||||||
|
<span>Отмена</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="el-button m-button el-button--small is-plain button-with-icon confirm-btn" style="color: #67C23A; border-color: #67C23A; display: flex; align-items: center;">
|
||||||
|
<i class="m-icon fa-redo-alt button-icon fad" style="font-size: 14px; margin-right: 6px;"></i>
|
||||||
|
<span>Отправить повторно</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция повторной отправки документов
|
||||||
|
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';
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
modalWrapper.innerHTML = `
|
||||||
|
<div class="el-dialog" style="width: 500px; margin-top: 25vh;">
|
||||||
|
<div class="el-dialog__header">
|
||||||
|
<span class="el-dialog__title">Электронные документы</span>
|
||||||
|
<button type="button" class="el-dialog__headerbtn" aria-label="Close">
|
||||||
|
<i class="el-dialog__close el-icon el-icon-close"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="el-dialog__body" style="text-align: center; padding: 40px;">
|
||||||
|
<i class="el-icon el-icon-document" style="font-size: 48px; color: #DCDFE6; margin-bottom: 16px;"></i>
|
||||||
|
<p style="color: #909399; font-size: 16px; margin: 0;">Нет документов, подписанных электронным способом</p>
|
||||||
|
</div>
|
||||||
|
<div class="el-dialog__footer" style="text-align: right;">
|
||||||
|
<button type="button" class="el-button m-button el-button--small is-plain button-with-icon close-btn">
|
||||||
|
<i class="m-icon fa-times button-icon fad" style="font-size: 14px; margin-right: 6px;"></i>
|
||||||
|
<span>Закрыть</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
const tableRows = data.map((singing, index) => {
|
||||||
|
// Определяем последний статус
|
||||||
|
const lastStatus = singing.statuses.reduce((max, status) =>
|
||||||
|
status.id > max.id ? status : max
|
||||||
|
, singing.statuses[0]);
|
||||||
|
|
||||||
|
const statusClass = getStatusClass(lastStatus.category);
|
||||||
|
const statusIcon = getStatusIcon(lastStatus.category);
|
||||||
|
const statusColor = lastStatus.category === 'completed' ? '#67C23A' :
|
||||||
|
lastStatus.category === 'processing' ? '#409EFF' :
|
||||||
|
lastStatus.category === 'error' ? '#F56C6C' : '#909399';
|
||||||
|
|
||||||
|
// Формируем ячейку с документами
|
||||||
|
const documentsHtml = singing.documents.map(doc => `
|
||||||
|
<div style="display: flex; align-items: flex-start; gap: 8px; margin-bottom: ${singing.documents.length > 1 ? '8px' : '0'}; padding: 4px 0;">
|
||||||
|
<div style="flex: 1; word-break: break-word; color: #606266;">
|
||||||
|
<span style="font-weight: 500; color: #303133;">№${doc.number}</span>
|
||||||
|
<span style="margin-left: 4px;">${doc.title}</span>
|
||||||
|
</div>
|
||||||
|
${doc.storagePath ? `
|
||||||
|
<button class="el-button el-button--text view-doc-btn"
|
||||||
|
data-title="${doc.title}"
|
||||||
|
data-path="${doc.storagePath}"
|
||||||
|
style="padding: 0 4px; min-height: auto; border: none;">
|
||||||
|
<i class="el-icon el-icon-view" style="color: #409EFF; font-size: 16px;"></i>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr style="border-bottom: 1px solid #EBEEF5;">
|
||||||
|
<td style="padding: 12px; vertical-align: top;">
|
||||||
|
<div style="font-weight: 500; color: #303133;">${formatDate(singing.created_at)}</div>
|
||||||
|
<div style="font-size: 12px; color: #909399; margin-top: 4px;">
|
||||||
|
<i class="el-icon el-icon-user" style="margin-right: 4px;"></i>
|
||||||
|
${singing.practitioner}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 12px; vertical-align: top; max-width: 550px;">
|
||||||
|
${documentsHtml}
|
||||||
|
</td>
|
||||||
|
<td style="padding: 12px; vertical-align: top;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 12px;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; flex: 1;">
|
||||||
|
<i class="el-icon ${statusIcon}" style="color: ${statusColor}; font-size: 16px;"></i>
|
||||||
|
<div>
|
||||||
|
<div style="color: ${statusColor};">${lastStatus.description}</div>
|
||||||
|
<div style="font-size: 12px; color: #909399; margin-top: 2px;">${formatDate(lastStatus.created_at)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
${singing.storagePath ? `
|
||||||
|
<button class="el-button el-button--text view-main-doc-btn"
|
||||||
|
data-title="${singing.title}"
|
||||||
|
data-path="${singing.storagePath}"
|
||||||
|
title="Просмотреть основной документ"
|
||||||
|
style="padding: 0; border: none;">
|
||||||
|
<i class="el-icon el-icon-document" style="color: #409EFF; font-size: 18px;"></i>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
${singing.statuses.length > 1 ? `
|
||||||
|
<button class="el-button el-button--text view-statuses-btn"
|
||||||
|
data-index="${index}"
|
||||||
|
title="История статусов"
|
||||||
|
style="padding: 0; border: none;">
|
||||||
|
<i class="el-icon el-icon-time" style="color: #909399; font-size: 18px;"></i>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
${lastStatus.category === 'processing' ? `
|
||||||
|
<div style="display: flex; gap: 4px;">
|
||||||
|
<button class="el-button el-button--text resend-btn"
|
||||||
|
data-index="${index}"
|
||||||
|
title="Повторно отправить документы"
|
||||||
|
style="padding: 0; border: none;">
|
||||||
|
<i class="el-icon el-icon-refresh" style="color: #67C23A; font-size: 18px;"></i>
|
||||||
|
</button>
|
||||||
|
<button class="el-button el-button--text revoke-btn"
|
||||||
|
data-index="${index}"
|
||||||
|
title="Отозвать документы"
|
||||||
|
style="padding: 0; border: none;">
|
||||||
|
<i class="el-icon el-icon-circle-close" style="color: #F56C6C; font-size: 18px;"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
modalWrapper.innerHTML = `
|
||||||
|
<div class="el-dialog" style="width: 90%; max-width: 1200px; margin-top: 5vh;">
|
||||||
|
<div class="el-dialog__header">
|
||||||
|
<span class="el-dialog__title">Электронные документы</span>
|
||||||
|
<button type="button" class="el-dialog__headerbtn" aria-label="Close">
|
||||||
|
<i class="el-dialog__close el-icon el-icon-close"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="el-dialog__body" style="padding: 20px;">
|
||||||
|
<div style="border: 1px solid #EBEEF5; border-radius: 4px; overflow: hidden;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background: #F5F7FA;">
|
||||||
|
<th style="padding: 12px; text-align: left; font-weight: 500; color: #909399;">Оформлено</th>
|
||||||
|
<th style="padding: 12px; text-align: left; font-weight: 500; color: #909399;">Документы</th>
|
||||||
|
<th style="padding: 12px; text-align: left; font-weight: 500; color: #909399;">Статус</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${tableRows}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="el-dialog__footer" style="text-align: right;">
|
||||||
|
<button type="button" class="el-button m-button el-button--small is-plain button-with-icon close-btn">
|
||||||
|
<i class="m-icon fa-times button-icon fad" style="font-size: 14px; margin-right: 6px;"></i>
|
||||||
|
<span>Закрыть</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(modalWrapper);
|
||||||
|
|
||||||
|
// Обработчики закрытия
|
||||||
|
const closeModal = () => modalWrapper.remove();
|
||||||
|
modalWrapper.querySelector('.el-dialog__headerbtn').addEventListener('click', closeModal);
|
||||||
|
modalWrapper.querySelector('.close-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);
|
||||||
|
|
||||||
|
// Обработчики для кнопок просмотра документов
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
modalWrapper.querySelectorAll('.view-doc-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const path = btn.dataset.path;
|
||||||
|
const title = btn.dataset.title;
|
||||||
|
viewDocument(path, title);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modalWrapper.querySelectorAll('.view-main-doc-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const path = btn.dataset.path;
|
||||||
|
const title = btn.dataset.title;
|
||||||
|
viewDocument(path, title);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modalWrapper.querySelectorAll('.view-statuses-btn').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const index = btn.dataset.index;
|
||||||
|
createStatusesModal(data[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modalWrapper.querySelectorAll('.revoke-btn').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const index = btn.dataset.index;
|
||||||
|
revokeDocuments(data[index], modalWrapper);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавляем обработчики для кнопок повторной отправки
|
||||||
|
modalWrapper.querySelectorAll('.resend-btn').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const index = btn.dataset.index;
|
||||||
|
resendDocuments(data[index], modalWrapper);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Основная функция подготовки документов
|
||||||
|
async function prepareDocuments() {
|
||||||
|
try {
|
||||||
|
showMessage('Загрузка документов...', 'info');
|
||||||
|
|
||||||
|
const response = await sendMessageToContent('prepareDocuments', {});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
createDocumentsTable(response.data);
|
||||||
|
} else {
|
||||||
|
showMessage('Ошибка загрузки документов: ' + (response.message || 'Неизвестная ошибка'), 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка подготовки документов:', error);
|
||||||
|
showMessage('Ошибка загрузки документов', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция добавления кнопки
|
||||||
|
function addDocsBtn() {
|
||||||
|
const docsBtn = document.createElement('button');
|
||||||
|
docsBtn.className = 'el-button m-button el-button--small is-plain button-with-icon';
|
||||||
|
docsBtn.innerHTML = `
|
||||||
|
<span>
|
||||||
|
<i class="m-icon fa-copy button-icon fad" style="font-size: 16px;"></i>
|
||||||
|
<span class="m-button__text use-indent">ЭДО</span>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const m_panel_footer = document.querySelector('.m-panel__footer');
|
||||||
|
if (m_panel_footer) {
|
||||||
|
m_panel_footer.insertAdjacentElement('afterbegin', docsBtn);
|
||||||
|
docsBtn.addEventListener('click', prepareDocuments);
|
||||||
|
} else {
|
||||||
|
// Если панель не найдена, пробуем еще раз через небольшую задержку
|
||||||
|
setTimeout(addDocsBtn, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем стили для статусов
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
.status-history-item:hover {
|
||||||
|
background-color: #F5F7FA;
|
||||||
|
}
|
||||||
|
.el-button--text {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
.el-button--text:hover {
|
||||||
|
background: none;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.el-button--text:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
// Запуск
|
||||||
|
setTimeout(() => {
|
||||||
|
addDocsBtn();
|
||||||
|
}, 500);
|
||||||
|
})();
|
||||||
Vendored
BIN
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Medods to N3.Health",
|
||||||
|
"version": "1.0",
|
||||||
|
"description": "Медодс. Электронное подписание документов",
|
||||||
|
"permissions": [
|
||||||
|
"activeTab",
|
||||||
|
"downloads",
|
||||||
|
"storage",
|
||||||
|
"scripting",
|
||||||
|
"declarativeNetRequest",
|
||||||
|
"offscreen"
|
||||||
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"http://*/*",
|
||||||
|
"https://*/*"
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
|
"options_page": "search.html",
|
||||||
|
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["*://*/*"],
|
||||||
|
"js": ["content.js"],
|
||||||
|
"run_at": "document_end"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": [
|
||||||
|
"metaData.js",
|
||||||
|
"singing.js",
|
||||||
|
"documents.js",
|
||||||
|
"userData.js",
|
||||||
|
"search.html"
|
||||||
|
],
|
||||||
|
"matches": ["<all_urls>"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"declarative_net_request": {
|
||||||
|
"rule_resources": [{
|
||||||
|
"id": "ruleset_1",
|
||||||
|
"enabled": true,
|
||||||
|
"path": "rules.json"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
// metaData.js
|
||||||
|
(function () {
|
||||||
|
const MAX_ATTEMPTS = 3;
|
||||||
|
const INTERVAL_MS = 1000;
|
||||||
|
|
||||||
|
let attempts = 0;
|
||||||
|
let done = false;
|
||||||
|
|
||||||
|
function sendMetaData() {
|
||||||
|
if (done) return;
|
||||||
|
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
if (window.gon) {
|
||||||
|
try {
|
||||||
|
done = true;
|
||||||
|
|
||||||
|
const metaData = {};
|
||||||
|
|
||||||
|
const clinicPhone = window.gon.application.current_clinic.phone1 || window.gon.application.current_clinic.phone2;
|
||||||
|
const clinicPhoneNormalized = `+7${clinicPhone.replace(/[^\d]/g, '').slice(1, 12)}`;
|
||||||
|
|
||||||
|
metaData.practitioner = {
|
||||||
|
familyName: window.gon.application.current_user.surname,
|
||||||
|
givenName: window.gon.application.current_user.name,
|
||||||
|
middleName: window.gon.application.current_user.second_name,
|
||||||
|
userIdLpu: window.gon.application.current_user.id,
|
||||||
|
telecom: [{
|
||||||
|
system: 'Email',
|
||||||
|
value: window.gon.application.current_clinic.email
|
||||||
|
}, {
|
||||||
|
system: 'Telephone',
|
||||||
|
value: clinicPhoneNormalized
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
patientPhone = window.gon.specific.client.phone;
|
||||||
|
const patientPhoneNormalized = `+7${patientPhone.replace(/[^\d]/g, '').slice(1, 12)}`;
|
||||||
|
|
||||||
|
metaData.patients = [{
|
||||||
|
idPatientMis: window.gon.specific.client.id,
|
||||||
|
familyName: window.gon.specific.client.surname,
|
||||||
|
givenName: window.gon.specific.client.name,
|
||||||
|
middleName: window.gon.specific.client.second_name,
|
||||||
|
birthDate: window.gon.specific.client.birthdate,
|
||||||
|
sex: window.gon.specific.client.sex_id === 1 ? 'male' : 'female',
|
||||||
|
telecom: [{
|
||||||
|
system: 'Telephone',
|
||||||
|
value: patientPhoneNormalized
|
||||||
|
}],
|
||||||
|
documentDto: [{
|
||||||
|
providerName: 'ПФР',
|
||||||
|
docN: window.gon.specific.client.snils,
|
||||||
|
docS: '',
|
||||||
|
documentName: 'СНИЛС',
|
||||||
|
idDocumentType: 223
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
|
||||||
|
if (window.gon.specific.client.email) {
|
||||||
|
metaData.patients[0].telecom.push({
|
||||||
|
system: 'Email',
|
||||||
|
value: window.gon.specific.client.email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
metaData.docs = window.gon.specific.docs.map(doc => ({
|
||||||
|
id: doc.id,
|
||||||
|
title: doc.title,
|
||||||
|
created_at: doc.created_at,
|
||||||
|
updated_at: doc.updated_at,
|
||||||
|
number: doc.number,
|
||||||
|
data: doc.data
|
||||||
|
}));
|
||||||
|
|
||||||
|
metaData.userData = {
|
||||||
|
id: window.gon.application.current_user.id,
|
||||||
|
login: window.gon.application.current_user.username,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Отправляем только один раз
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'metaData',
|
||||||
|
payload: metaData
|
||||||
|
},
|
||||||
|
'*'
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in metaData:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempts >= MAX_ATTEMPTS) {
|
||||||
|
console.warn('Max attempts reached for metaData');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(sendMetaData, INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем после небольшой задержки
|
||||||
|
setTimeout(sendMetaData, 100);
|
||||||
|
})();
|
||||||
+837
@@ -0,0 +1,837 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Медодс. Электронное подписание</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--el-color-primary: #409eff;
|
||||||
|
--el-color-success: #67c23a;
|
||||||
|
--el-color-warning: #e6a23c;
|
||||||
|
--el-color-danger: #f56c6c;
|
||||||
|
--el-color-info: #909399;
|
||||||
|
--el-text-color-primary: #303133;
|
||||||
|
--el-text-color-regular: #606266;
|
||||||
|
--el-text-color-secondary: #909399;
|
||||||
|
--el-text-color-placeholder: #c0c4cc;
|
||||||
|
--el-border-color-light: #e4e7ed;
|
||||||
|
--el-border-color-lighter: #ebeef5;
|
||||||
|
--el-border-color-extra-light: #f2f6fc;
|
||||||
|
--el-background-color-base: #f5f7fa;
|
||||||
|
--el-font-family: Verdana, Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 500px;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: var(--el-font-family);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
margin: 0;
|
||||||
|
background: var(--el-background-color-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accordion */
|
||||||
|
.accordion {
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3px 12px;
|
||||||
|
background: var(--el-background-color-base);
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
user-select: none;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-header:hover {
|
||||||
|
background: var(--el-border-color-extra-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-title {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-chevron {
|
||||||
|
font-size: 12px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-content {
|
||||||
|
padding: 12px;
|
||||||
|
background: white;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion.expanded .accordion-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion.expanded .accordion-chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success {
|
||||||
|
color: var(--el-color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-warning {
|
||||||
|
color: var(--el-color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Server info - compact */
|
||||||
|
.server-info {
|
||||||
|
font-size: 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
/*margin-bottom: 8px;*/
|
||||||
|
min-height: 24px;
|
||||||
|
padding-right: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
width: 75px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-right: 5px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-value {
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
flex: 1;
|
||||||
|
padding: 2px 0;
|
||||||
|
padding-left: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-input {
|
||||||
|
flex: 1;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: white;
|
||||||
|
outline: none;
|
||||||
|
display: none;
|
||||||
|
margin-left: 5px;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-input.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-input:focus {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-input.error {
|
||||||
|
border-color: var(--el-color-danger);
|
||||||
|
background: #fff5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-value.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action buttons - positioned to the right of fields */
|
||||||
|
.action-buttons {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
background: white;
|
||||||
|
padding: 0;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background: var(--el-border-color-extra-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn:hover {
|
||||||
|
background: #ebf8ff;
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
color: var(--el-color-success);
|
||||||
|
border-color: var(--el-color-success);
|
||||||
|
background: #f0fff4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:hover:not(.disabled) {
|
||||||
|
background: #e1f3d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn.disabled {
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
border-color: var(--el-border-color-light);
|
||||||
|
background: var(--el-background-color-base);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover {
|
||||||
|
background: #fed7d7;
|
||||||
|
border-color: var(--el-color-danger);
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auth warning */
|
||||||
|
.auth-warning {
|
||||||
|
display: none;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fdf6ec;
|
||||||
|
border: 1px solid #faecd8;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--el-color-warning);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-warning.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filters section - two rows */
|
||||||
|
.filters-section {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper {
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper:hover {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper.focused {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-label {
|
||||||
|
position: absolute;
|
||||||
|
top: -7px;
|
||||||
|
left: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
background: white;
|
||||||
|
padding: 0 4px;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-select {
|
||||||
|
width: 100%;
|
||||||
|
height: 34px;
|
||||||
|
padding: 8px 24px 8px 8px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
font-family: var(--el-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-select:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table header section */
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-documents-btn {
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-family: var(--el-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-documents-btn:hover {
|
||||||
|
background: var(--el-background-color-base);
|
||||||
|
border-color: var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 28px;
|
||||||
|
padding: 2px 28px 2px 8px;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: white;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: var(--el-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-clear {
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 0;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
display: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-clear.visible {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-clear:hover {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
background: var(--el-background-color-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Refresh button */
|
||||||
|
.refresh-btn {
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-family: var(--el-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:hover {
|
||||||
|
background: var(--el-background-color-base);
|
||||||
|
border-color: var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table section */
|
||||||
|
.table-section {
|
||||||
|
display: none;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table */
|
||||||
|
.table-container {
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
table-layout: fixed;
|
||||||
|
font-family: var(--el-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table thead {
|
||||||
|
background: var(--el-background-color-base);
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th {
|
||||||
|
padding: 10px 8px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th:nth-child(1) {
|
||||||
|
width: 30%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th:nth-child(2) {
|
||||||
|
width: 45%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th:nth-child(3) {
|
||||||
|
width: 25%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr {
|
||||||
|
border-bottom: 1px solid var(--el-border-color-extra-light);
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr:hover {
|
||||||
|
background: var(--el-background-color-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table td {
|
||||||
|
padding: 10px 8px;
|
||||||
|
vertical-align: top;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table td:nth-child(1) {
|
||||||
|
text-align: left;
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table td:nth-child(2) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table td:nth-child(3) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
margin-bottom: 0;
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: var(--el-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-date {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.3;
|
||||||
|
display: block;
|
||||||
|
font-family: var(--el-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-docs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-item {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
background: #fdf6ec;
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 220px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
text-align: center;
|
||||||
|
white-space: normal;
|
||||||
|
border: 1px solid #faecd8;
|
||||||
|
font-family: var(--el-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
text-align: right;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-family: var(--el-font-family);
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
cursor: help;
|
||||||
|
position: relative;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-date {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-align: right;
|
||||||
|
display: block;
|
||||||
|
font-family: var(--el-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-completed {
|
||||||
|
background: #f0f9eb;
|
||||||
|
color: var(--el-color-success);
|
||||||
|
border: 1px solid #e1f3d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-processing {
|
||||||
|
background: #ecf5ff;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
border: 1px solid #d9ecff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
background: #fef0f0;
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
border: 1px solid #fde2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-waiting {
|
||||||
|
background: #fdf6ec;
|
||||||
|
color: var(--el-color-warning);
|
||||||
|
border: 1px solid #faecd8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.empty-state {
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: var(--el-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility classes */
|
||||||
|
.visible {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-section.visible {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h3>Медодс. Электронное подписание</h3>
|
||||||
|
|
||||||
|
<!-- Auth warning -->
|
||||||
|
<div id="authWarning" class="auth-warning">
|
||||||
|
Для загрузки данных необходимо войти в Медодс
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table section -->
|
||||||
|
<div id="tableSection" class="table-section">
|
||||||
|
<!-- Table header with search and buttons -->
|
||||||
|
<div class="table-header">
|
||||||
|
<button id="allDocumentsBtn" class="all-documents-btn" title="Все документы">
|
||||||
|
🔎 Расширенный поиск
|
||||||
|
</button>
|
||||||
|
<div class="search-container">
|
||||||
|
<input type="text" id="searchInput" class="search-input" placeholder="Поиск ...">
|
||||||
|
<button id="searchClear" class="search-clear" title="Очистить поиск">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button id="refreshBtn" class="refresh-btn">
|
||||||
|
<span>🔄 Обновить</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters in two rows -->
|
||||||
|
<div class="filters-section">
|
||||||
|
<div class="filters-row">
|
||||||
|
<div class="filter-group">
|
||||||
|
<div class="select-wrapper" id="esiaFilterWrapper">
|
||||||
|
<span class="m-label">Подписание</span>
|
||||||
|
<select id="esiaFilter" class="m-select">
|
||||||
|
<option value="all">Все</option>
|
||||||
|
<option value="esia">Только ЕСИА</option>
|
||||||
|
<option value="not_esia">Только не ЕСИА</option>
|
||||||
|
</select>
|
||||||
|
<span class="select-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<div class="select-wrapper" id="senderFilterWrapper">
|
||||||
|
<span class="m-label">Отправитель</span>
|
||||||
|
<select id="senderFilter" class="m-select">
|
||||||
|
<option value="all">Все</option>
|
||||||
|
<option value="my">Только мои</option>
|
||||||
|
<option value="not_my">Только не мои</option>
|
||||||
|
</select>
|
||||||
|
<span class="select-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filters-row">
|
||||||
|
<div class="filter-group">
|
||||||
|
<div class="select-wrapper" id="statusFilterWrapper">
|
||||||
|
<span class="m-label">Статус</span>
|
||||||
|
<select id="statusFilter" class="m-select">
|
||||||
|
<option value="all">Все</option>
|
||||||
|
<option value="processing">В обработке</option>
|
||||||
|
<option value="completed">Завершено</option>
|
||||||
|
</select>
|
||||||
|
<span class="select-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<div class="select-wrapper" id="periodFilterWrapper">
|
||||||
|
<span class="m-label">Период</span>
|
||||||
|
<select id="periodFilter" class="m-select">
|
||||||
|
<option value="today">Сегодня</option>
|
||||||
|
<option value="3days">За 3 дня</option>
|
||||||
|
<option value="7days">За 7 дней</option>
|
||||||
|
</select>
|
||||||
|
<span class="select-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="data-table" id="documentsTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ФИО клиента</th>
|
||||||
|
<th>Документы на подписание</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tableBody">
|
||||||
|
<!-- Данные будут добавляться динамически -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div id="emptyState" class="empty-state hidden">
|
||||||
|
Нет документов
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Server settings accordion -->
|
||||||
|
<div id="serverAccordion" class="accordion">
|
||||||
|
<div class="accordion-header" id="accordionHeader">
|
||||||
|
<span class="accordion-icon" id="accordionIcon">⚠️</span>
|
||||||
|
<span class="accordion-title" id="accordionTitle">Настройка сервера</span>
|
||||||
|
<span class="accordion-chevron">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="accordion-content" id="accordionContent">
|
||||||
|
<div class="server-info">
|
||||||
|
<div class="server-field">
|
||||||
|
<span class="server-label">IP Сервера:</span>
|
||||||
|
<span id="currentServerIp" class="server-value">не настроен</span>
|
||||||
|
<input id="editServerIp" class="server-input" type="text" placeholder="127.0.0.1"
|
||||||
|
maxlength="15">
|
||||||
|
</div>
|
||||||
|
<div class="server-field">
|
||||||
|
<span class="server-label">Порт:</span>
|
||||||
|
<span id="currentServerPort" class="server-value">не настроен</span>
|
||||||
|
<input id="editServerPort" class="server-input" type="number" placeholder="8000" min="1"
|
||||||
|
max="65535">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit button (shows when viewing settings) -->
|
||||||
|
<div class="action-buttons" id="viewButtons">
|
||||||
|
<button class="action-btn edit-btn" id="editButton" title="Редактировать">⚙️</button>
|
||||||
|
<button class="action-btn edit-btn hidden" id="adminButton"
|
||||||
|
title="Администрирование">🔑</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Save/Cancel buttons (shows when editing) -->
|
||||||
|
<div class="action-buttons hidden" id="editButtons">
|
||||||
|
<button class="action-btn save-btn disabled" id="saveOptions" title="Сохранить">✓</button>
|
||||||
|
<button class="action-btn cancel-btn" id="cancelOptions" title="Отменить">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,620 @@
|
|||||||
|
// popup.js
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
// Состояние фильтров
|
||||||
|
let filters = {
|
||||||
|
esia: 'all',
|
||||||
|
sender: 'all',
|
||||||
|
status: 'all',
|
||||||
|
period: 'today'
|
||||||
|
};
|
||||||
|
|
||||||
|
let filtersChanged = false;
|
||||||
|
let allStatuses = [];
|
||||||
|
let currentUserId = null;
|
||||||
|
|
||||||
|
// DOM элементы
|
||||||
|
const authWarning = document.getElementById('authWarning');
|
||||||
|
const tableSection = document.getElementById('tableSection');
|
||||||
|
const tableBody = document.getElementById('tableBody');
|
||||||
|
const emptyState = document.getElementById('emptyState');
|
||||||
|
const refreshBtn = document.getElementById('refreshBtn');
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
const searchClear = document.getElementById('searchClear');
|
||||||
|
const allDocumentsBtn = document.getElementById('allDocumentsBtn');
|
||||||
|
|
||||||
|
// Фильтры
|
||||||
|
const esiaFilter = document.getElementById('esiaFilter');
|
||||||
|
const senderFilter = document.getElementById('senderFilter');
|
||||||
|
const statusFilter = document.getElementById('statusFilter');
|
||||||
|
const periodFilter = document.getElementById('periodFilter');
|
||||||
|
|
||||||
|
// Настройки сервера
|
||||||
|
const serverAccordion = document.getElementById('serverAccordion');
|
||||||
|
const accordionHeader = document.getElementById('accordionHeader');
|
||||||
|
const accordionIcon = document.getElementById('accordionIcon');
|
||||||
|
const accordionTitle = document.getElementById('accordionTitle');
|
||||||
|
const currentServerIp = document.getElementById('currentServerIp');
|
||||||
|
const currentServerPort = document.getElementById('currentServerPort');
|
||||||
|
const editServerIp = document.getElementById('editServerIp');
|
||||||
|
const editServerPort = document.getElementById('editServerPort');
|
||||||
|
const viewButtons = document.getElementById('viewButtons');
|
||||||
|
const editButtons = document.getElementById('editButtons');
|
||||||
|
const editButton = document.getElementById('editButton');
|
||||||
|
const adminButton = document.getElementById('adminButton');
|
||||||
|
const saveOptions = document.getElementById('saveOptions');
|
||||||
|
const cancelOptions = document.getElementById('cancelOptions');
|
||||||
|
|
||||||
|
// Вспомогательные функции
|
||||||
|
function formatDate(dateString) {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('ru-RU', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusClass(category) {
|
||||||
|
switch (category) {
|
||||||
|
case 'completed': return 'status-completed';
|
||||||
|
case 'processing': return 'status-processing';
|
||||||
|
case 'error': return 'status-error';
|
||||||
|
default: return 'status-waiting';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusIcon(category) {
|
||||||
|
switch (category) {
|
||||||
|
case 'completed': return '✓';
|
||||||
|
case 'processing': return '⋯';
|
||||||
|
case 'error': return '✗';
|
||||||
|
default: return '?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMessage(message, type = 'success', duration = 2000) {
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 10000;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: ${type === 'success' ? '#f0f9eb' : '#fef0f0'};
|
||||||
|
border: 1px solid ${type === 'success' ? '#e1f3d8' : '#fde2e2'};
|
||||||
|
color: ${type === 'success' ? '#67c23a' : '#f56c6c'};
|
||||||
|
font-size: 13px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
|
||||||
|
`;
|
||||||
|
messageEl.textContent = message;
|
||||||
|
document.body.appendChild(messageEl);
|
||||||
|
setTimeout(() => messageEl.remove(), duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для выделения кнопки обновления
|
||||||
|
function highlightRefreshButton() {
|
||||||
|
refreshBtn.style.animation = 'pulse 1.5s infinite';
|
||||||
|
refreshBtn.style.backgroundColor = '#e6f7ff';
|
||||||
|
refreshBtn.style.borderColor = '#91d5ff';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetRefreshButton() {
|
||||||
|
refreshBtn.style.animation = 'none';
|
||||||
|
refreshBtn.style.backgroundColor = '';
|
||||||
|
refreshBtn.style.borderColor = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем стили
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); background-color: #ffffff; }
|
||||||
|
50% { transform: scale(1.05); background-color: #e6f7ff; box-shadow: 0 0 10px rgba(64, 158, 255, 0.5); }
|
||||||
|
100% { transform: scale(1); background-color: #ffffff; }
|
||||||
|
}
|
||||||
|
.clickable-doc {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.clickable-doc:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
background-color: #f0f9eb !important;
|
||||||
|
}
|
||||||
|
.doc-view-btn {
|
||||||
|
margin-left: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border: none;
|
||||||
|
background: #ecf5ff;
|
||||||
|
color: #409eff;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.doc-view-btn:hover {
|
||||||
|
background: #d9ecff;
|
||||||
|
}
|
||||||
|
.status-container {
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
.document-status {
|
||||||
|
max-width: 140px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.doctor-name {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
// Отправка сообщений в background
|
||||||
|
async function sendMessageToBackground(action, data) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.runtime.sendMessage({ action, data }, (response) => {
|
||||||
|
resolve(response || { success: false, message: 'Нет ответа от background' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохранение фильтров в storage
|
||||||
|
function saveFiltersToStorage() {
|
||||||
|
chrome.storage.local.set({ documentFilters: filters });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загрузка фильтров из storage
|
||||||
|
async function loadFiltersFromStorage() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.storage.local.get(['documentFilters'], (result) => {
|
||||||
|
if (result.documentFilters) {
|
||||||
|
filters = { ...filters, ...result.documentFilters };
|
||||||
|
|
||||||
|
// Применяем фильтры к элементам управления
|
||||||
|
esiaFilter.value = filters.esia;
|
||||||
|
senderFilter.value = filters.sender;
|
||||||
|
statusFilter.value = filters.status;
|
||||||
|
periodFilter.value = filters.period;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение статусов с сервера
|
||||||
|
async function loadStatuses() {
|
||||||
|
try {
|
||||||
|
refreshBtn.disabled = true;
|
||||||
|
refreshBtn.innerHTML = '<span>🔄 Загрузка...</span>';
|
||||||
|
|
||||||
|
// Получаем ID пользователя из storage
|
||||||
|
await getCurrentUserId();
|
||||||
|
|
||||||
|
// Если нет ID пользователя - считаем что не авторизованы
|
||||||
|
if (!currentUserId) {
|
||||||
|
authWarning.classList.add('visible');
|
||||||
|
tableSection.classList.remove('visible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формируем данные для отправки на сервер (без search)
|
||||||
|
const requestData = {
|
||||||
|
userIdLpu: currentUserId,
|
||||||
|
filters: {
|
||||||
|
esia: filters.esia,
|
||||||
|
sender: filters.sender,
|
||||||
|
status: filters.status,
|
||||||
|
period: filters.period
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Отправляем запрос с фильтрами
|
||||||
|
const response = await sendMessageToBackground('getStatuses', requestData);
|
||||||
|
|
||||||
|
if (response.success && Array.isArray(response.data)) {
|
||||||
|
allStatuses = response.data;
|
||||||
|
authWarning.classList.remove('visible');
|
||||||
|
tableSection.classList.add('visible');
|
||||||
|
|
||||||
|
// Применяем поиск на клиенте
|
||||||
|
applyClientFilters();
|
||||||
|
} else {
|
||||||
|
showMessage('Ошибка загрузки: ' + (response.message || 'Неизвестная ошибка'), 'error');
|
||||||
|
allStatuses = [];
|
||||||
|
renderTable([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showMessage('Ошибка загрузки данных', 'error');
|
||||||
|
allStatuses = [];
|
||||||
|
renderTable([]);
|
||||||
|
} finally {
|
||||||
|
refreshBtn.disabled = false;
|
||||||
|
refreshBtn.innerHTML = '<span>🔄 Обновить</span>';
|
||||||
|
resetRefreshButton();
|
||||||
|
filtersChanged = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение ID текущего пользователя из storage
|
||||||
|
async function getCurrentUserId() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.storage.local.get(['userData'], (result) => {
|
||||||
|
if (result.userData && result.userData.id) {
|
||||||
|
currentUserId = result.userData.id;
|
||||||
|
resolve(currentUserId);
|
||||||
|
} else {
|
||||||
|
currentUserId = null;
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Применение клиентских фильтров (поиск)
|
||||||
|
function applyClientFilters() {
|
||||||
|
let filtered = [...allStatuses];
|
||||||
|
|
||||||
|
// Поиск по тексту (на клиенте)
|
||||||
|
const searchText = searchInput.value.toLowerCase().trim();
|
||||||
|
if (searchText) {
|
||||||
|
filtered = filtered.filter(item => {
|
||||||
|
const patientName = (item.patientName || '').toLowerCase();
|
||||||
|
const documents = item.documents?.map(d => `${d.number} ${d.title}`).join(' ').toLowerCase() || '';
|
||||||
|
return patientName.includes(searchText) || documents.includes(searchText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTable(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Просмотр документа
|
||||||
|
async function viewDocument(documentPath, title) {
|
||||||
|
try {
|
||||||
|
const response = await sendMessageToBackground('getDocument', { documentPath });
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const newWindow = window.open(url, '_blank');
|
||||||
|
if (!newWindow) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = 'document.pdf';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => window.URL.revokeObjectURL(url), 1000);
|
||||||
|
} else {
|
||||||
|
showMessage('Ошибка загрузки документа', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showMessage('Ошибка просмотра документа', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Рендер таблицы
|
||||||
|
function renderTable(data) {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
emptyState.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const documentsHtml = item.documents?.map(doc => {
|
||||||
|
const isClickable = lastStatus?.category !== 'error';
|
||||||
|
const baseClass = isClickable ? 'doc-item clickable-doc' : 'doc-item';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="${baseClass}"
|
||||||
|
${isClickable ? `data-path="${doc.storagePath}" data-title="${doc.title}"` : ''}
|
||||||
|
style="${isClickable ? 'cursor: pointer;' : ''} display: flex; align-items: center; justify-content: space-between;">
|
||||||
|
<span style="flex: 1; text-align: left; word-break: break-word;">№${doc.number} ${doc.title?.replace(/\s+/g, ' ')}</span>
|
||||||
|
${isClickable ? '<span style="margin-left: 4px;">📄</span>' : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const hasMainDocument = item.storagePath && !item.storagePath.includes('null');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="document-name">${item.patientName || 'Неизвестно'}</div>
|
||||||
|
<div class="document-date">${formatDate(item.created_at)}</div>
|
||||||
|
<div class="doctor-name" title="${item.userName || ''}">
|
||||||
|
<span>👤 ${item.userName || 'Неизвестно'}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="document-docs">
|
||||||
|
${documentsHtml}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="status-container">
|
||||||
|
<div class="document-status ${statusClass}"
|
||||||
|
title="${lastStatus?.description || ''}">
|
||||||
|
<span style="margin-right: 4px;">${statusIcon}</span>
|
||||||
|
${lastStatus?.name || 'Неизвестно'}
|
||||||
|
</div>
|
||||||
|
<div class="status-date">${formatDate(lastStatus?.created_at || item.created_at)}</div>
|
||||||
|
${hasMainDocument ? `
|
||||||
|
<button class="doc-view-btn" data-path="${item.storagePath}" data-title="Основной документ">
|
||||||
|
📄 Просмотр
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
tableBody.innerHTML = rows;
|
||||||
|
|
||||||
|
// Добавляем обработчики для кликабельных документов
|
||||||
|
document.querySelectorAll('.clickable-doc').forEach(el => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const path = el.dataset.path;
|
||||||
|
const title = el.dataset.title;
|
||||||
|
if (path) viewDocument(path, title);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчики для кнопок просмотра
|
||||||
|
document.querySelectorAll('.doc-view-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const path = btn.dataset.path;
|
||||||
|
const title = btn.dataset.title;
|
||||||
|
if (path) viewDocument(path, title);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчики фильтров
|
||||||
|
function setupFilters() {
|
||||||
|
const filterElements = [esiaFilter, senderFilter, statusFilter, periodFilter];
|
||||||
|
|
||||||
|
filterElements.forEach(filter => {
|
||||||
|
filter.addEventListener('change', () => {
|
||||||
|
// Обновляем состояние фильтров
|
||||||
|
filters[filter.id.replace('Filter', '')] = filter.value;
|
||||||
|
filtersChanged = true;
|
||||||
|
highlightRefreshButton();
|
||||||
|
saveFiltersToStorage();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', () => {
|
||||||
|
// Для поиска не показываем уведомление о необходимости обновления
|
||||||
|
// применяем фильтрацию сразу на клиенте
|
||||||
|
applyClientFilters();
|
||||||
|
searchClear.classList.toggle('visible', searchInput.value.length > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
searchClear.addEventListener('click', () => {
|
||||||
|
searchInput.value = '';
|
||||||
|
searchClear.classList.remove('visible');
|
||||||
|
applyClientFilters();
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshBtn.addEventListener('click', () => {
|
||||||
|
// Сохраняем текущие фильтры и загружаем данные
|
||||||
|
saveFiltersToStorage();
|
||||||
|
loadStatuses();
|
||||||
|
});
|
||||||
|
|
||||||
|
allDocumentsBtn.addEventListener('click', () => {
|
||||||
|
chrome.runtime.openOptionsPage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Настройки сервера
|
||||||
|
function setupServerSettings() {
|
||||||
|
// Загрузка сохраненных настроек
|
||||||
|
chrome.storage.local.get(['serverIp', 'serverPort'], (result) => {
|
||||||
|
if (result.serverIp) {
|
||||||
|
currentServerIp.textContent = result.serverIp;
|
||||||
|
editServerIp.value = result.serverIp;
|
||||||
|
}
|
||||||
|
if (result.serverPort) {
|
||||||
|
currentServerPort.textContent = result.serverPort;
|
||||||
|
editServerPort.value = result.serverPort;
|
||||||
|
}
|
||||||
|
if (result.serverIp && result.serverPort) {
|
||||||
|
accordionIcon.textContent = '✅';
|
||||||
|
accordionTitle.textContent = 'Сервер настроен';
|
||||||
|
adminButton.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Аккордеон
|
||||||
|
accordionHeader.addEventListener('click', () => {
|
||||||
|
serverAccordion.classList.toggle('expanded');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Редактирование
|
||||||
|
editButton.addEventListener('click', () => {
|
||||||
|
viewButtons.classList.add('hidden');
|
||||||
|
editButtons.classList.remove('hidden');
|
||||||
|
|
||||||
|
currentServerIp.classList.add('hidden');
|
||||||
|
currentServerPort.classList.add('hidden');
|
||||||
|
editServerIp.classList.add('visible');
|
||||||
|
editServerPort.classList.add('visible');
|
||||||
|
|
||||||
|
saveOptions.classList.add('disabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Валидация полей
|
||||||
|
[editServerIp, editServerPort].forEach(input => {
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
const ipValid = /^(\d{1,3}\.){3}\d{1,3}$/.test(editServerIp.value) || editServerIp.value === '';
|
||||||
|
const portValid = editServerPort.value > 0 && editServerPort.value < 65536;
|
||||||
|
|
||||||
|
editServerIp.classList.toggle('error', !ipValid && editServerIp.value !== '');
|
||||||
|
editServerPort.classList.toggle('error', !portValid && editServerPort.value !== '');
|
||||||
|
|
||||||
|
if (ipValid && portValid && editServerIp.value && editServerPort.value) {
|
||||||
|
saveOptions.classList.remove('disabled');
|
||||||
|
} else {
|
||||||
|
saveOptions.classList.add('disabled');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Сохранение с проверкой health
|
||||||
|
saveOptions.addEventListener('click', async () => {
|
||||||
|
if (saveOptions.classList.contains('disabled')) return;
|
||||||
|
|
||||||
|
const ip = editServerIp.value;
|
||||||
|
const port = parseInt(editServerPort.value);
|
||||||
|
|
||||||
|
// Показываем индикатор загрузки на кнопке
|
||||||
|
const originalText = saveOptions.textContent;
|
||||||
|
saveOptions.textContent = '⋯';
|
||||||
|
saveOptions.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Проверяем health endpoint
|
||||||
|
const healthUrl = `http://${ip}:${port}/api/health`;
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 5000); // Таймаут 5 секунд
|
||||||
|
|
||||||
|
const response = await fetch(healthUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Проверяем что получили ожидаемый ответ
|
||||||
|
if (data.status !== 'healthy') {
|
||||||
|
throw new Error('Сервер вернул некорректный ответ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если все ок - сохраняем настройки
|
||||||
|
chrome.storage.local.set({ serverIp: ip, serverPort: port }, () => {
|
||||||
|
currentServerIp.textContent = ip;
|
||||||
|
currentServerPort.textContent = port;
|
||||||
|
|
||||||
|
viewButtons.classList.remove('hidden');
|
||||||
|
editButtons.classList.add('hidden');
|
||||||
|
|
||||||
|
currentServerIp.classList.remove('hidden');
|
||||||
|
currentServerPort.classList.remove('hidden');
|
||||||
|
editServerIp.classList.remove('visible', 'error');
|
||||||
|
editServerPort.classList.remove('visible', 'error');
|
||||||
|
|
||||||
|
accordionIcon.textContent = '✅';
|
||||||
|
accordionTitle.textContent = 'Сервер настроен';
|
||||||
|
adminButton.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Показываем успешное сообщение
|
||||||
|
showMessage('Сервер успешно подключен', 'success', 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Health check failed:', error);
|
||||||
|
|
||||||
|
// Определяем тип ошибки для понятного сообщения
|
||||||
|
let errorMessage = 'Ошибка подключения к серверу';
|
||||||
|
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
errorMessage = 'Сервер не отвечает (таймаут 5 сек)';
|
||||||
|
} else if (error.message.includes('Failed to fetch')) {
|
||||||
|
errorMessage = 'Сервер недоступен. Проверьте IP и порт';
|
||||||
|
} else {
|
||||||
|
errorMessage = error.message || 'Неизвестная ошибка';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем уведомление об ошибке
|
||||||
|
showMessage(`❌ ${errorMessage}`, 'error', 3000);
|
||||||
|
|
||||||
|
// Подсвечиваем поля с ошибкой
|
||||||
|
editServerIp.classList.add('error');
|
||||||
|
editServerPort.classList.add('error');
|
||||||
|
|
||||||
|
// Возвращаем иконку на кнопке
|
||||||
|
saveOptions.textContent = originalText;
|
||||||
|
saveOptions.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отмена
|
||||||
|
cancelOptions.addEventListener('click', () => {
|
||||||
|
viewButtons.classList.remove('hidden');
|
||||||
|
editButtons.classList.add('hidden');
|
||||||
|
|
||||||
|
currentServerIp.classList.remove('hidden');
|
||||||
|
currentServerPort.classList.remove('hidden');
|
||||||
|
editServerIp.classList.remove('visible', 'error');
|
||||||
|
editServerPort.classList.remove('visible', 'error');
|
||||||
|
|
||||||
|
editServerIp.value = currentServerIp.textContent;
|
||||||
|
editServerPort.value = currentServerPort.textContent;
|
||||||
|
});
|
||||||
|
|
||||||
|
//Администрирование
|
||||||
|
adminButton.addEventListener('click', () => {
|
||||||
|
chrome.tabs.create({ url: `http://${currentServerIp.textContent}:${currentServerPort.textContent}/` });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка авторизации
|
||||||
|
async function checkAuth() {
|
||||||
|
const userId = await getCurrentUserId();
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
authWarning.classList.remove('visible');
|
||||||
|
tableSection.classList.add('visible');
|
||||||
|
// Загружаем фильтры из storage и делаем первый запрос
|
||||||
|
await loadFiltersFromStorage();
|
||||||
|
loadStatuses();
|
||||||
|
} else {
|
||||||
|
authWarning.classList.add('visible');
|
||||||
|
tableSection.classList.remove('visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
setupServerSettings();
|
||||||
|
setupFilters();
|
||||||
|
checkAuth();
|
||||||
|
});
|
||||||
|
})();
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"priority": 1,
|
||||||
|
"action": {
|
||||||
|
"type": "modifyHeaders",
|
||||||
|
"responseHeaders": [
|
||||||
|
{
|
||||||
|
"header": "Access-Control-Allow-Origin",
|
||||||
|
"operation": "set",
|
||||||
|
"value": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"condition": {
|
||||||
|
"urlFilter": "*",
|
||||||
|
"resourceTypes": [
|
||||||
|
"image",
|
||||||
|
"stylesheet",
|
||||||
|
"font",
|
||||||
|
"script",
|
||||||
|
"xmlhttprequest"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
+256
@@ -0,0 +1,256 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="icons/icon128.png" type="image/png" sizes="128x128">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Медодс. Расширенный поиск документов</title>
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link rel="stylesheet" href="bootstrap/bootstrap.min.css">
|
||||||
|
<!-- Bootstrap Icons -->
|
||||||
|
<link rel="stylesheet" href="bootstrap/bootstrap-icons.css">
|
||||||
|
<!-- Минимальные кастомные стили только для специфических случаев -->
|
||||||
|
<style>
|
||||||
|
/* Только критически необходимые стили, которые нельзя реализовать через Bootstrap */
|
||||||
|
.table-container {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead.sticky-top {
|
||||||
|
z-index: 1020;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-item {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-item:hover {
|
||||||
|
background-color: var(--bs-primary-bg-subtle) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Фикс для модального окна */
|
||||||
|
.modal.show {
|
||||||
|
display: block;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Для пустого состояния */
|
||||||
|
.empty-state-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
color: var(--bs-gray-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Для лоадера */
|
||||||
|
.spinner-wrapper {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container py-4">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-body d-flex justify-content-between align-items-center">
|
||||||
|
<h1 class="h4 mb-0">
|
||||||
|
Расширенный поиск документов
|
||||||
|
<small class="text-muted fs-6 ms-2">электронное подписание</small>
|
||||||
|
</h1>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button class="btn btn-outline-secondary" id="clearFiltersBtn">
|
||||||
|
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||||
|
Сбросить фильтры
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" id="searchBtn">
|
||||||
|
<i class="bi bi-search me-1"></i>
|
||||||
|
Найти
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters Section -->
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<i class="bi bi-sliders2 text-primary me-2"></i>
|
||||||
|
<h2 class="h6 mb-0">Фильтры поиска</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<!-- Период -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small text-uppercase text-muted fw-semibold">Период</label>
|
||||||
|
<select class="form-select" id="periodSelect">
|
||||||
|
<option value="today">Сегодня</option>
|
||||||
|
<option value="3days">За 3 дня</option>
|
||||||
|
<option value="7days">За неделю</option>
|
||||||
|
<option value="30days">За месяц</option>
|
||||||
|
<option value="90days">За квартал</option>
|
||||||
|
<option value="custom">Произвольный</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Статус -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small text-uppercase text-muted fw-semibold">Статус подписания</label>
|
||||||
|
<select class="form-select" id="statusSelect">
|
||||||
|
<option value="all">Все статусы</option>
|
||||||
|
<option value="processing">В обработке</option>
|
||||||
|
<option value="completed">Завершено</option>
|
||||||
|
<option value="error">Ошибки (отмены)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Тип подписания -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small text-uppercase text-muted fw-semibold">Тип подписания</label>
|
||||||
|
<select class="form-select" id="signatureTypeSelect">
|
||||||
|
<option value="all">Все типы</option>
|
||||||
|
<option value="esia">УКЭП для ЕСИА</option>
|
||||||
|
<option value="not_esia">МЧД для других</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced Filters Toggle -->
|
||||||
|
<div class="mt-3">
|
||||||
|
<button class="btn btn-link text-decoration-none p-0" type="button" id="advancedToggle">
|
||||||
|
<i class="bi bi-chevron-down me-1" id="advancedIcon"></i>
|
||||||
|
Расширенные фильтры
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced Filters -->
|
||||||
|
<div class="mt-3" id="advancedFilters" style="display: none;">
|
||||||
|
<div class="row g-3">
|
||||||
|
<!-- Отправитель -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Отправитель (фамилия)</label>
|
||||||
|
<input type="text" class="form-control" id="senderInput" placeholder="Фамилия отправителя">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Пациент -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Пациент (фамилия)</label>
|
||||||
|
<input type="text" class="form-control" id="patientInput" placeholder="Фамилия пациента">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Номер документа -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Номер документа</label>
|
||||||
|
<input type="number" class="form-control" id="documentNumberInput"
|
||||||
|
placeholder="Только цифры" min="1" step="1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range for Custom Period -->
|
||||||
|
<div class="mt-3" id="customDateRange" style="display: none;">
|
||||||
|
<hr class="my-3">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">С</label>
|
||||||
|
<input type="date" class="form-control" id="dateFrom">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">По</label>
|
||||||
|
<input type="date" class="form-control" id="dateTo">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results Section -->
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="bi bi-file-text text-primary me-2"></i>
|
||||||
|
<h2 class="h6 mb-0">Результаты поиска</h2>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small" id="resultsStats">
|
||||||
|
Найдено: 0
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="sticky-top bg-white">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 15%">Дата</th>
|
||||||
|
<th style="width: 15%">Пациент</th>
|
||||||
|
<th style="width: 35%">Документы</th>
|
||||||
|
<th style="width: 20%">Статус</th>
|
||||||
|
<th style="width: 15%">Отправитель</th>
|
||||||
|
<th style="width: 10%">Тип</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tableBody">
|
||||||
|
<!-- Данные будут загружаться динамически -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div class="text-center py-5" id="emptyState">
|
||||||
|
<i class="bi bi-search empty-state-icon d-block mb-3"></i>
|
||||||
|
<p class="text-muted mb-1">Нет данных для отображения</p>
|
||||||
|
<p class="text-muted small">Используйте фильтры для поиска документов</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="card-footer bg-white py-3 d-flex justify-content-end align-items-center gap-3" id="pagination"
|
||||||
|
style="display: none;">
|
||||||
|
<div class="text-muted small" id="paginationInfo">
|
||||||
|
Страница 1 из 1
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" id="prevPage" disabled>
|
||||||
|
<i class="bi bi-chevron-left"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" id="nextPage" disabled>
|
||||||
|
<i class="bi bi-chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal for Status History -->
|
||||||
|
<div class="modal fade" id="statusHistoryModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">История статусов</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="statusHistoryBody">
|
||||||
|
<!-- Динамическое содержимое -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS Bundle with Popper -->
|
||||||
|
<script src="bootstrap/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="search.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,704 @@
|
|||||||
|
// 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);
|
||||||
|
})();
|
||||||
+808
@@ -0,0 +1,808 @@
|
|||||||
|
// singing.js
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
// Глобальная переменная для хранения текущего уведомления
|
||||||
|
let currentMessageEl = null;
|
||||||
|
let messageTimeout = null;
|
||||||
|
let isSending = false;
|
||||||
|
|
||||||
|
// Функция для отображения сообщений
|
||||||
|
function showMessage(message, type = 'success', duration = 3000) {
|
||||||
|
// Удаляем предыдущее уведомление, если оно есть
|
||||||
|
if (currentMessageEl && currentMessageEl.parentNode) {
|
||||||
|
clearTimeout(messageTimeout);
|
||||||
|
document.body.removeChild(currentMessageEl);
|
||||||
|
currentMessageEl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.className = `el-message el-message--${type}`;
|
||||||
|
messageEl.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 9999;
|
||||||
|
min-width: 380px;
|
||||||
|
padding: 15px 15px 15px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
background-color: ${type === 'success' ? '#f0f9eb' :
|
||||||
|
type === 'info' ? '#f4f4f5' : '#fef0f0'};
|
||||||
|
border: 1px solid ${type === 'success' ? '#e1f3d8' :
|
||||||
|
type === 'info' ? '#e9e9eb' : '#fde2e2'};
|
||||||
|
color: ${type === 'success' ? '#67c23a' :
|
||||||
|
type === 'info' ? '#909399' : '#f56c6c'};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const iconClass = type === 'success' ? 'el-icon-success' :
|
||||||
|
type === 'info' ? 'el-icon-info' : 'el-icon-error';
|
||||||
|
const iconColor = type === 'success' ? '#67c23a' :
|
||||||
|
type === 'info' ? '#909399' : '#f56c6c';
|
||||||
|
|
||||||
|
messageEl.innerHTML = `
|
||||||
|
<i class="el-icon ${iconClass}" style="font-size: 16px; color: ${iconColor}; margin-right: 10px;"></i>
|
||||||
|
<span style="font-size: 14px;">${message}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(messageEl);
|
||||||
|
currentMessageEl = messageEl;
|
||||||
|
|
||||||
|
// Скрываем через указанное время
|
||||||
|
messageTimeout = setTimeout(() => {
|
||||||
|
if (currentMessageEl === messageEl && messageEl.parentNode) {
|
||||||
|
document.body.removeChild(messageEl);
|
||||||
|
currentMessageEl = null;
|
||||||
|
}
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция отправки сообщения
|
||||||
|
function sendMessageToContent(type, payload) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
reject(new Error('Таймаут ожидания ответа'));
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
const messageHandler = (event) => {
|
||||||
|
if (
|
||||||
|
event.source !== window ||
|
||||||
|
!event.data ||
|
||||||
|
event.data.source !== 'medods-extension' ||
|
||||||
|
event.data.type !== `${type}Response`
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.removeEventListener('message', messageHandler);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
resolve(event.data.payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', messageHandler);
|
||||||
|
|
||||||
|
window.postMessage({
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: type,
|
||||||
|
payload: payload,
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик для финальных результатов от background
|
||||||
|
function setupBackgroundResultHandler() {
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
if (
|
||||||
|
event.source !== window ||
|
||||||
|
!event.data ||
|
||||||
|
event.data.source !== 'medods-extension' ||
|
||||||
|
event.data.type !== 'backgroundProcessingResult'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Получен финальный результат от background:', event.data);
|
||||||
|
|
||||||
|
const result = event.data.payload;
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showMessage(`Успешно отправлено документов: ${result.sentCount || 1}`, 'success');
|
||||||
|
} else {
|
||||||
|
const errorMessage = result.message || 'Неизвестная ошибка';
|
||||||
|
showMessage(`Ошибка: ${errorMessage}`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка совместимости документов
|
||||||
|
function checkDocumentsCompatibility(documents) {
|
||||||
|
if (documents.length === 0) {
|
||||||
|
return { compatible: true, type: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasIds = false;
|
||||||
|
let hasNonIds = false;
|
||||||
|
|
||||||
|
for (const doc of documents) {
|
||||||
|
if (doc.title.includes('ИДС')) {
|
||||||
|
hasIds = true;
|
||||||
|
} else {
|
||||||
|
hasNonIds = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasIds && hasNonIds) {
|
||||||
|
return {
|
||||||
|
compatible: false,
|
||||||
|
type: 'error',
|
||||||
|
message: 'Нельзя одновременно отправлять документы требующие для подписания авторизацию ЕСИА (ГосУслуги) - "ИДС" и другие документы. Выберите документы одного типа.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const docType = hasIds ? 'через ЕСИА (ГосУслуги)' : 'ПЭП';
|
||||||
|
return {
|
||||||
|
compatible: true,
|
||||||
|
type: docType
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция получения выбранных документов
|
||||||
|
function getSelectedDocuments() {
|
||||||
|
const table = document.querySelector('.m-table.m-table-generator.m-si-generator__table');
|
||||||
|
if (!table) return [];
|
||||||
|
|
||||||
|
const checkboxes = table.querySelectorAll('.el-checkbox__original');
|
||||||
|
const selectedDocs = [];
|
||||||
|
|
||||||
|
checkboxes.forEach((checkbox) => {
|
||||||
|
if (checkbox.checked) {
|
||||||
|
const row = checkbox.closest('.m-table-row');
|
||||||
|
if (row) {
|
||||||
|
const rowData = {};
|
||||||
|
const numberCell = row.querySelector('.col__number .m-table-row-cell__struct');
|
||||||
|
const titleCell = row.querySelector('.col__title .m-table-row-cell__struct');
|
||||||
|
|
||||||
|
if (numberCell) rowData.number = numberCell.textContent.trim();
|
||||||
|
if (titleCell) rowData.title = titleCell.textContent.trim();
|
||||||
|
|
||||||
|
if (rowData.number && rowData.title) {
|
||||||
|
selectedDocs.push(rowData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return selectedDocs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция проверки ограничения в 5 документов
|
||||||
|
function validateDocumentCount(documents) {
|
||||||
|
if (documents.length === 0) {
|
||||||
|
return { valid: false, message: 'Выберите хотя бы один документ для отправки' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (documents.length > 5) {
|
||||||
|
return { valid: false, message: 'Нельзя отправить более 5 документов за раз' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true, message: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция создания модального окна предварительной проверки
|
||||||
|
function showPreSingingDialog(documents, preSingingData) {
|
||||||
|
// Проверяем данные
|
||||||
|
const signature = preSingingData.signature || {};
|
||||||
|
const inProgress = preSingingData.inProgress || [];
|
||||||
|
const complete = preSingingData.complete || [];
|
||||||
|
const daysRemainingControl = preSingingData.daysRemainingControl || 30;
|
||||||
|
|
||||||
|
// Определяем тип документов
|
||||||
|
const hasIds = documents.some(doc => doc.title.includes('ИДС'));
|
||||||
|
|
||||||
|
// Проверяем соответствие типов
|
||||||
|
let typeMatch = false;
|
||||||
|
|
||||||
|
if (signature.type === 'eSignature' && hasIds) {
|
||||||
|
typeMatch = true;
|
||||||
|
} else if (signature.type === 'eAttorney' && !hasIds) {
|
||||||
|
typeMatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем срок действия
|
||||||
|
let expirationInfo = '';
|
||||||
|
let expirationFormatted = '';
|
||||||
|
let expirationClass = 'info';
|
||||||
|
let expirationTitle = '';
|
||||||
|
let isExpired = false;
|
||||||
|
let daysRemaining = 0;
|
||||||
|
|
||||||
|
if (signature.expiration) {
|
||||||
|
const expirationDate = new Date(signature.expiration);
|
||||||
|
const today = new Date();
|
||||||
|
const timeDiff = expirationDate - today;
|
||||||
|
daysRemaining = Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||||
|
|
||||||
|
// Форматируем дату
|
||||||
|
const dateFormatter = new Intl.DateTimeFormat('ru-RU', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
expirationFormatted = dateFormatter.format(expirationDate);
|
||||||
|
expirationInfo = expirationFormatted;
|
||||||
|
|
||||||
|
// Определяем тип для заголовка
|
||||||
|
if (signature.type === 'eSignature') {
|
||||||
|
expirationTitle = 'Электронная подпись';
|
||||||
|
} else if (signature.type === 'eAttorney') {
|
||||||
|
expirationTitle = 'Электронная доверенность';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysRemaining < 0) {
|
||||||
|
expirationClass = 'danger';
|
||||||
|
isExpired = true;
|
||||||
|
} else if (daysRemaining < daysRemainingControl) {
|
||||||
|
expirationClass = 'warning';
|
||||||
|
} else {
|
||||||
|
expirationClass = 'success';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем статусы документов
|
||||||
|
const documentsWithStatus = documents.map(doc => {
|
||||||
|
const docNumber = parseInt(doc.number, 10);
|
||||||
|
let status = 'ready';
|
||||||
|
let statusClass = 'success';
|
||||||
|
let statusIcon = 'el-icon-success';
|
||||||
|
|
||||||
|
if (inProgress.includes(docNumber)) {
|
||||||
|
status = 'in-progress';
|
||||||
|
statusClass = 'info';
|
||||||
|
statusIcon = 'el-icon-time';
|
||||||
|
} else if (complete.includes(docNumber)) {
|
||||||
|
status = 'complete';
|
||||||
|
statusClass = 'primary';
|
||||||
|
statusIcon = 'el-icon-check';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...doc,
|
||||||
|
status,
|
||||||
|
statusClass,
|
||||||
|
statusIcon,
|
||||||
|
isEsia: doc.title.includes('ИДС')
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Определяем, можно ли отправлять
|
||||||
|
let canSend = typeMatch && !isExpired;
|
||||||
|
|
||||||
|
// Проверяем статусы документов
|
||||||
|
const hasInProgress = documentsWithStatus.some(doc => doc.status === 'in-progress');
|
||||||
|
const hasComplete = documentsWithStatus.some(doc => doc.status === 'complete');
|
||||||
|
|
||||||
|
if (hasInProgress || hasComplete) {
|
||||||
|
canSend = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если нет signature - нельзя отправлять
|
||||||
|
if (!signature.type) {
|
||||||
|
canSend = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если типы не соответствуют
|
||||||
|
if (!typeMatch && signature.type) {
|
||||||
|
canSend = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем элементы модального окна напрямую
|
||||||
|
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;
|
||||||
|
|
||||||
|
const dialog = document.createElement('div');
|
||||||
|
dialog.className = 'el-dialog';
|
||||||
|
dialog.style.cssText = 'margin-top: 15vh; width: 700px; z-index: 2002; position: relative; margin: 15vh auto 50px; background: #fff; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);';
|
||||||
|
|
||||||
|
// Создаем заголовок
|
||||||
|
const dialogHeader = document.createElement('div');
|
||||||
|
dialogHeader.className = 'el-dialog__header';
|
||||||
|
|
||||||
|
const dialogTitle = document.createElement('span');
|
||||||
|
dialogTitle.className = 'el-dialog__title';
|
||||||
|
dialogTitle.style.cssText = 'line-height: 24px; font-size: 18px; color: #303133;';
|
||||||
|
dialogTitle.textContent = 'Подготовка документов к отправке для электронного подписания';
|
||||||
|
|
||||||
|
const closeButton = document.createElement('button');
|
||||||
|
closeButton.type = 'button';
|
||||||
|
closeButton.className = 'el-dialog__headerbtn';
|
||||||
|
closeButton.style.cssText = 'position: absolute; top: 20px; right: 20px; padding: 0; background: 0 0; border: none; outline: 0; cursor: pointer; font-size: 16px;';
|
||||||
|
|
||||||
|
const closeIcon = document.createElement('i');
|
||||||
|
closeIcon.className = 'el-dialog__close el-icon el-icon-close';
|
||||||
|
|
||||||
|
closeButton.appendChild(closeIcon);
|
||||||
|
dialogHeader.appendChild(dialogTitle);
|
||||||
|
dialogHeader.appendChild(closeButton);
|
||||||
|
|
||||||
|
// Создаем тело диалога
|
||||||
|
const dialogBody = document.createElement('div');
|
||||||
|
dialogBody.className = 'el-dialog__body';
|
||||||
|
dialogBody.style.cssText = 'padding: 15px; color: #606266; font-size: 14px;';
|
||||||
|
|
||||||
|
// Создаем контент
|
||||||
|
const dialogContent = document.createElement('div');
|
||||||
|
dialogContent.className = 'dialog-content';
|
||||||
|
|
||||||
|
// Предупреждения (ближе к заголовку)
|
||||||
|
if (!canSend) {
|
||||||
|
const warningsSection = document.createElement('div');
|
||||||
|
warningsSection.className = 'warnings-section';
|
||||||
|
warningsSection.style.cssText = 'margin-bottom: 15px;';
|
||||||
|
|
||||||
|
// Собираем все предупреждения в один массив
|
||||||
|
const warnings = [];
|
||||||
|
|
||||||
|
if (!signature.type) {
|
||||||
|
warnings.push({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Нет прав на отправку',
|
||||||
|
message: 'У вас нет прав на электронное подписание документов'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysRemaining < 0) {
|
||||||
|
let typeName = 'документа';
|
||||||
|
if (signature.type === 'eSignature') {
|
||||||
|
typeName = 'электронной подписи';
|
||||||
|
} else if (signature.type === 'eAttorney') {
|
||||||
|
typeName = 'электронной доверенности';
|
||||||
|
}
|
||||||
|
|
||||||
|
warnings.push({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Срок действия истек',
|
||||||
|
message: `Срок действия ${typeName} истек ${Math.abs(daysRemaining)} дней назад`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!typeMatch && signature.type) {
|
||||||
|
const availableType = signature.type === 'eSignature' ? 'электронная подпись' : 'электронная доверенность';
|
||||||
|
const neededType = hasIds ? 'электронная подпись' : 'электронная доверенность';
|
||||||
|
|
||||||
|
warnings.push({
|
||||||
|
type: 'warning',
|
||||||
|
title: 'Несоответствие типов документов',
|
||||||
|
message: `Для подписания подготовлены документы для которых требуется "${neededType}", но у вас доступна "${availableType}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasInProgress || hasComplete) {
|
||||||
|
warnings.push({
|
||||||
|
type: 'warning',
|
||||||
|
title: 'Невозможно отправить документы',
|
||||||
|
message: 'Среди выбранных документов есть уже подписанные (✔️) или направленные для подписания (🕒)'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отображаем все предупреждения
|
||||||
|
warnings.forEach(warning => {
|
||||||
|
const warningEl = document.createElement('div');
|
||||||
|
warningEl.className = `el-alert el-alert--${warning.type} is-light`;
|
||||||
|
warningEl.style.cssText = 'margin-bottom: 8px; padding: 8px; border-radius: 4px;';
|
||||||
|
|
||||||
|
const warningContent = document.createElement('div');
|
||||||
|
warningContent.className = 'el-alert__content';
|
||||||
|
|
||||||
|
const warningTitle = document.createElement('span');
|
||||||
|
warningTitle.className = 'el-alert__title';
|
||||||
|
warningTitle.style.cssText = 'font-size: 13px; color: ' +
|
||||||
|
(warning.type === 'error' ? '#f56c6c' : '#e6a23c') + ';';
|
||||||
|
warningTitle.textContent = warning.title;
|
||||||
|
|
||||||
|
const warningDesc = document.createElement('p');
|
||||||
|
warningDesc.className = 'el-alert__description';
|
||||||
|
warningDesc.style.cssText = 'font-size: 12px; margin-top: 3px; color: ' +
|
||||||
|
(warning.type === 'error' ? '#f56c6c' : '#e6a23c') + ';';
|
||||||
|
warningDesc.textContent = warning.message;
|
||||||
|
|
||||||
|
warningContent.appendChild(warningTitle);
|
||||||
|
warningContent.appendChild(warningDesc);
|
||||||
|
warningEl.appendChild(warningContent);
|
||||||
|
warningsSection.appendChild(warningEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogContent.appendChild(warningsSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Список документов
|
||||||
|
const documentsSection = document.createElement('div');
|
||||||
|
documentsSection.className = 'documents-section';
|
||||||
|
|
||||||
|
const documentsTitle = document.createElement('h4');
|
||||||
|
documentsTitle.style.cssText = 'margin: 0 0 10px 0; color: #303133; font-size: 16px;';
|
||||||
|
documentsTitle.textContent = 'Список документов:';
|
||||||
|
|
||||||
|
const documentsList = document.createElement('div');
|
||||||
|
documentsList.style.cssText = 'border: 1px solid #EBEEF5; border-radius: 4px; padding: 0 10px; max-height: 200px; overflow-y: auto;';
|
||||||
|
|
||||||
|
// Добавляем документы в список
|
||||||
|
documentsWithStatus.forEach((doc, index) => {
|
||||||
|
const docItem = document.createElement('div');
|
||||||
|
|
||||||
|
// Для последнего элемента не добавляем нижнюю границу
|
||||||
|
if (index === documentsWithStatus.length - 1) {
|
||||||
|
docItem.style.cssText = 'display: flex; align-items: center; padding: 8px 0;';
|
||||||
|
} else {
|
||||||
|
docItem.style.cssText = 'display: flex; align-items: center; padding: 8px 0; border-bottom: 1px solid #F0F0F0;';
|
||||||
|
}
|
||||||
|
// Статус (иконка слева)
|
||||||
|
const docStatusIcon = document.createElement('div');
|
||||||
|
docStatusIcon.style.cssText = 'flex: 0 0 24px; margin-right: 8px;';
|
||||||
|
|
||||||
|
const statusIcon = document.createElement('i');
|
||||||
|
statusIcon.className = doc.statusIcon;
|
||||||
|
statusIcon.style.cssText = `font-size: 16px; color: ${doc.status === 'ready' ? '#67c23a' :
|
||||||
|
doc.status === 'in-progress' ? '#909399' : '#409eff'
|
||||||
|
};`;
|
||||||
|
|
||||||
|
docStatusIcon.appendChild(statusIcon);
|
||||||
|
|
||||||
|
// Номер документа (стилизован в зависимости от статуса)
|
||||||
|
const docNumber = document.createElement('div');
|
||||||
|
docNumber.style.cssText = 'flex: 0 0 60px;';
|
||||||
|
|
||||||
|
const numberTag = document.createElement('span');
|
||||||
|
|
||||||
|
if (doc.status === 'ready') {
|
||||||
|
numberTag.className = 'el-tag el-tag--success el-tag--small';
|
||||||
|
numberTag.style.cssText = 'display: inline-block; padding: 0 7px; height: 24px; line-height: 22px; font-size: 12px; border-width: 1px; border-style: solid; border-radius: 4px; box-sizing: border-box; white-space: nowrap;';
|
||||||
|
} else if (doc.status === 'in-progress') {
|
||||||
|
numberTag.className = 'el-tag el-tag--info el-tag--small';
|
||||||
|
numberTag.style.cssText = 'display: inline-block; padding: 0 7px; height: 24px; line-height: 22px; font-size: 12px; border-width: 1px; border-style: solid; border-radius: 4px; box-sizing: border-box; white-space: nowrap;';
|
||||||
|
} else {
|
||||||
|
numberTag.className = 'el-tag el-tag--primary el-tag--small';
|
||||||
|
numberTag.style.cssText = 'display: inline-block; padding: 0 7px; height: 24px; line-height: 22px; font-size: 12px; border-width: 1px; border-style: solid; border-radius: 4px; box-sizing: border-box; white-space: nowrap;';
|
||||||
|
}
|
||||||
|
|
||||||
|
numberTag.textContent = `№${doc.number}`;
|
||||||
|
|
||||||
|
docNumber.appendChild(numberTag);
|
||||||
|
|
||||||
|
// Название документа (занимает все оставшееся пространство)
|
||||||
|
const docTitle = document.createElement('div');
|
||||||
|
docTitle.style.cssText = 'flex: 1; color: #606266; font-size: 13px; padding: 0 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;';
|
||||||
|
docTitle.textContent = doc.title;
|
||||||
|
|
||||||
|
docItem.appendChild(docStatusIcon);
|
||||||
|
docItem.appendChild(docNumber);
|
||||||
|
docItem.appendChild(docTitle);
|
||||||
|
|
||||||
|
documentsList.appendChild(docItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
documentsSection.appendChild(documentsTitle);
|
||||||
|
documentsSection.appendChild(documentsList);
|
||||||
|
|
||||||
|
dialogContent.appendChild(documentsSection);
|
||||||
|
dialogBody.appendChild(dialogContent);
|
||||||
|
|
||||||
|
// Создаем футер
|
||||||
|
const dialogFooter = document.createElement('div');
|
||||||
|
dialogFooter.className = 'el-dialog__footer';
|
||||||
|
dialogFooter.style.cssText = 'display: flex; justify-content: space-between; align-items: center;';
|
||||||
|
|
||||||
|
// Левая часть футера (информация о сроке действия разрешения на подпись)
|
||||||
|
const footerLeft = document.createElement('div');
|
||||||
|
footerLeft.style.cssText = 'flex: 1; display: flex; flex-direction: column; align-items: flex-start;';
|
||||||
|
|
||||||
|
if (signature.expiration && signature.type) {
|
||||||
|
// Тип разрешения
|
||||||
|
const typeContainer = document.createElement('div');
|
||||||
|
typeContainer.style.cssText = 'margin-bottom: 5px; margin-top: 0px;';
|
||||||
|
|
||||||
|
const typeText = document.createElement('span');
|
||||||
|
typeText.style.cssText = 'font-size: 12px; font-weight: 500; color: #606266;';
|
||||||
|
|
||||||
|
if (signature.type === 'eSignature') {
|
||||||
|
typeText.textContent = 'Электронная подпись';
|
||||||
|
} else if (signature.type === 'eAttorney') {
|
||||||
|
typeText.textContent = 'Электронная доверенность';
|
||||||
|
}
|
||||||
|
|
||||||
|
typeContainer.appendChild(typeText);
|
||||||
|
|
||||||
|
// Срок действия
|
||||||
|
const expirationContainer = document.createElement('div');
|
||||||
|
expirationContainer.style.cssText = 'display: flex; align-items: center; flex-wrap: wrap; margin-bottom: 0px;';
|
||||||
|
|
||||||
|
const expirationLabel = document.createElement('span');
|
||||||
|
expirationLabel.style.cssText = 'font-size: 12px; color: #606266; margin-right: 5px;';
|
||||||
|
expirationLabel.textContent = 'Срок действия:';
|
||||||
|
|
||||||
|
const expirationDate = document.createElement('span');
|
||||||
|
expirationDate.style.cssText = 'font-size: 12px; color: #606266; margin-right: 8px;';
|
||||||
|
expirationDate.textContent = expirationFormatted;
|
||||||
|
|
||||||
|
expirationContainer.appendChild(expirationLabel);
|
||||||
|
expirationContainer.appendChild(expirationDate);
|
||||||
|
|
||||||
|
// Остаточный срок (показываем справа от даты если меньше 30 дней)
|
||||||
|
if (daysRemaining >= 0 && daysRemaining < 30) {
|
||||||
|
const warningIcon = document.createElement('i');
|
||||||
|
warningIcon.className = 'el-icon-warning';
|
||||||
|
warningIcon.style.cssText = 'font-size: 14px; color: #e6a23c; margin-left: 4px; margin-right: 4px;';
|
||||||
|
|
||||||
|
const remainingText = document.createElement('span');
|
||||||
|
remainingText.style.cssText = 'font-size: 12px; color: #e6a23c; font-weight: 500;';
|
||||||
|
remainingText.textContent = `осталось ${daysRemaining} ${getDaysText(daysRemaining)}`;
|
||||||
|
|
||||||
|
typeContainer.appendChild(warningIcon);
|
||||||
|
typeContainer.appendChild(remainingText);
|
||||||
|
}
|
||||||
|
|
||||||
|
footerLeft.appendChild(typeContainer);
|
||||||
|
footerLeft.appendChild(expirationContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вспомогательная функция для склонения дней
|
||||||
|
function getDaysText(days) {
|
||||||
|
const lastDigit = days % 10;
|
||||||
|
const lastTwoDigits = days % 100;
|
||||||
|
|
||||||
|
if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
|
||||||
|
return 'дней';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (lastDigit) {
|
||||||
|
case 1: return 'день';
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
case 4: return 'дня';
|
||||||
|
default: return 'дней';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Правая часть футера (кнопки)
|
||||||
|
const footerRight = document.createElement('div');
|
||||||
|
footerRight.className = 'dialog-footer';
|
||||||
|
footerRight.style.cssText = 'display: flex; align-items: center; padding-top: 10px;';
|
||||||
|
|
||||||
|
// Кнопка "Отмена" - стиль как у кнопки на главной странице
|
||||||
|
const cancelBtn = document.createElement('button');
|
||||||
|
cancelBtn.type = 'button';
|
||||||
|
cancelBtn.className = 'el-button m-button el-button--small is-plain button-with-icon cancel-button';
|
||||||
|
|
||||||
|
const cancelIcon = document.createElement('i');
|
||||||
|
cancelIcon.className = 'm-icon fa-times button-icon fad';
|
||||||
|
cancelIcon.style.cssText = 'font-size: 14px; margin-right: 6px;';
|
||||||
|
|
||||||
|
const cancelText = document.createElement('span');
|
||||||
|
cancelText.textContent = 'Отмена';
|
||||||
|
|
||||||
|
cancelBtn.appendChild(cancelIcon);
|
||||||
|
cancelBtn.appendChild(cancelText);
|
||||||
|
|
||||||
|
// Кнопка "Отправить документы" - стиль как у кнопки "подписать электронно"
|
||||||
|
const sendBtn = document.createElement('button');
|
||||||
|
sendBtn.type = 'button';
|
||||||
|
|
||||||
|
if (canSend) {
|
||||||
|
sendBtn.className = 'el-button m-button el-button--small is-plain send-button button-with-icon';
|
||||||
|
} else {
|
||||||
|
sendBtn.className = 'el-button m-button el-button--small is-plain send-button button-with-icon is-disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendIcon = document.createElement('i');
|
||||||
|
sendIcon.className = 'm-icon fa-file-signature button-icon fad';
|
||||||
|
sendIcon.style.cssText = 'font-size: 14px; margin-right: 6px;';
|
||||||
|
|
||||||
|
const sendText = document.createElement('span');
|
||||||
|
sendText.textContent = 'Отправить документы';
|
||||||
|
|
||||||
|
sendBtn.appendChild(sendIcon);
|
||||||
|
sendBtn.appendChild(sendText);
|
||||||
|
|
||||||
|
sendBtn.disabled = !canSend;
|
||||||
|
|
||||||
|
footerRight.appendChild(cancelBtn);
|
||||||
|
footerRight.appendChild(sendBtn);
|
||||||
|
|
||||||
|
dialogFooter.appendChild(footerLeft);
|
||||||
|
dialogFooter.appendChild(footerRight);
|
||||||
|
|
||||||
|
// Собираем диалог
|
||||||
|
dialog.appendChild(dialogHeader);
|
||||||
|
dialog.appendChild(dialogBody);
|
||||||
|
dialog.appendChild(dialogFooter);
|
||||||
|
|
||||||
|
// Собираем модальное окно
|
||||||
|
modalWrapper.appendChild(backdrop);
|
||||||
|
modalWrapper.appendChild(dialog);
|
||||||
|
document.body.appendChild(modalWrapper);
|
||||||
|
|
||||||
|
// Обработчик отправки
|
||||||
|
async function handleSend() {
|
||||||
|
if (!canSend) return;
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
|
||||||
|
// Отправляем документы на подписание
|
||||||
|
try {
|
||||||
|
const docNumbers = documents.map(doc => parseInt(doc.number, 10));
|
||||||
|
|
||||||
|
const result = await sendMessageToContent('sendDocuments', {
|
||||||
|
docNumbers: docNumbers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result && result.response) {
|
||||||
|
if (result.response.status === 'processing') {
|
||||||
|
showMessage('Документы приняты в обработку. Ожидайте результата...', 'info', 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при отправке:', error);
|
||||||
|
showMessage(`Ошибка: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
if (modalWrapper.parentNode) {
|
||||||
|
document.body.removeChild(modalWrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Назначаем обработчики
|
||||||
|
closeButton.addEventListener('click', closeModal);
|
||||||
|
cancelBtn.addEventListener('click', closeModal);
|
||||||
|
backdrop.addEventListener('click', closeModal);
|
||||||
|
|
||||||
|
if (canSend) {
|
||||||
|
sendBtn.addEventListener('click', handleSend);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Закрытие по ESC
|
||||||
|
const escHandler = (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeModal();
|
||||||
|
document.removeEventListener('keydown', escHandler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', escHandler);
|
||||||
|
|
||||||
|
// Удаляем обработчик ESC при закрытии
|
||||||
|
const originalClose = closeModal;
|
||||||
|
closeModal = () => {
|
||||||
|
document.removeEventListener('keydown', escHandler);
|
||||||
|
originalClose();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция предварительной проверки документов
|
||||||
|
async function preSingingCheck(documents) {
|
||||||
|
try {
|
||||||
|
// Проверяем количество документов
|
||||||
|
const countValidation = validateDocumentCount(documents);
|
||||||
|
if (!countValidation.valid) {
|
||||||
|
showMessage(countValidation.message, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем совместимость документов
|
||||||
|
const compatibility = checkDocumentsCompatibility(documents);
|
||||||
|
if (!compatibility.compatible) {
|
||||||
|
showMessage(compatibility.message, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем запрос на предварительную проверку
|
||||||
|
showMessage('Проверка документов...', 'info');
|
||||||
|
|
||||||
|
// Отправляем запрос в content.js, чтобы он собрал и отправил данные
|
||||||
|
const result = await sendMessageToContent('preSingingCheck', {
|
||||||
|
// Передаем только флаг о типе документов для совместимости
|
||||||
|
docNumbers: documents.map(doc => parseInt(doc.number))
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result && result.success && result.data) {
|
||||||
|
// Показываем диалоговое окно с результатами проверки
|
||||||
|
showPreSingingDialog(documents, result.data);
|
||||||
|
} else {
|
||||||
|
showMessage('Ошибка при проверке документов', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка предварительной проверки:', error);
|
||||||
|
showMessage(`Ошибка: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Функция добавления кнопки
|
||||||
|
function addSendBtn() {
|
||||||
|
const sendBtn = document.createElement('button');
|
||||||
|
sendBtn.className = 'el-button m-button el-button--small is-disabled is-plain button-with-icon';
|
||||||
|
sendBtn.innerHTML = `
|
||||||
|
<span>
|
||||||
|
<i class="m-icon fa-file-signature button-icon fad" style="font-size: 16px;"></i>
|
||||||
|
<span class="m-button__text use-indent">Подписать электронно</span>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
sendBtn.disabled = true;
|
||||||
|
|
||||||
|
// Локальный обработчик для кнопки
|
||||||
|
async function handleSendClick() {
|
||||||
|
if (isSending) {
|
||||||
|
console.log('Уже идет отправка, игнорируем клик');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedDocs = getSelectedDocuments();
|
||||||
|
|
||||||
|
// Запускаем предварительную проверку
|
||||||
|
preSingingCheck(selectedDocs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkSelectedDocuments() {
|
||||||
|
return getSelectedDocuments().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateButtonState() {
|
||||||
|
const hasSelectedDocs = checkSelectedDocuments();
|
||||||
|
sendBtn.disabled = !hasSelectedDocs || isSending;
|
||||||
|
if (sendBtn.disabled) {
|
||||||
|
sendBtn.classList.add('is-disabled');
|
||||||
|
} else {
|
||||||
|
sendBtn.classList.remove('is-disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupTableObserver() {
|
||||||
|
const table = document.querySelector('.m-table.m-table-generator.m-si-generator__table');
|
||||||
|
if (!table) {
|
||||||
|
console.warn('Таблица документов не найдена');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.addEventListener('change', (e) => {
|
||||||
|
if (e.target.classList.contains('el-checkbox__original')) {
|
||||||
|
updateButtonState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
table.addEventListener('click', (e) => {
|
||||||
|
if (e.target.closest('.el-checkbox') ||
|
||||||
|
e.target.closest('.el-checkbox__inner')) {
|
||||||
|
setTimeout(updateButtonState, 10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(updateButtonState, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
const m_panel_footer = document.querySelector('.m-panel__footer');
|
||||||
|
if (m_panel_footer) {
|
||||||
|
m_panel_footer.prepend(sendBtn);
|
||||||
|
|
||||||
|
sendBtn.addEventListener('click', handleSendClick);
|
||||||
|
|
||||||
|
setupTableObserver();
|
||||||
|
updateButtonState();
|
||||||
|
setupBackgroundResultHandler();
|
||||||
|
} else {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск
|
||||||
|
setTimeout(() => {
|
||||||
|
addSendBtn();
|
||||||
|
}, 500);
|
||||||
|
})();
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
// userData.js
|
||||||
|
(function () {
|
||||||
|
function sendUserData() {
|
||||||
|
|
||||||
|
if (window.gon) {
|
||||||
|
try {
|
||||||
|
const userData = {
|
||||||
|
id: window.gon.application.current_user.id
|
||||||
|
};
|
||||||
|
|
||||||
|
// Отправляем только один раз
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
source: 'medods-extension',
|
||||||
|
type: 'userData',
|
||||||
|
payload: userData
|
||||||
|
},
|
||||||
|
'*'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in sendUserData:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем после небольшой задержки
|
||||||
|
setTimeout(sendUserData, 100);
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user