сервисный журнал, фикс мультиклика на инструменты
This commit is contained in:
+342
-4
@@ -264,7 +264,7 @@ function fillTab(tabId, tabData) {
|
||||
renderJurnalToolkitsTab(tabId, tabData);
|
||||
break;
|
||||
case 'jurnal_service':
|
||||
renderSimpleTab(tabId, tabData, 'Сервисный журнал');
|
||||
renderJurnalServicesTab(tabId, tabData);
|
||||
break;
|
||||
case 'users':
|
||||
renderSimpleTab(tabId, tabData, 'Пользователи системы');
|
||||
@@ -1454,10 +1454,16 @@ function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', catego
|
||||
}).join('');
|
||||
|
||||
const cards = container.querySelectorAll('.toolkit-card');
|
||||
let activeModal = null;
|
||||
|
||||
cards.forEach(card => {
|
||||
card.addEventListener('click', async event => {
|
||||
if (activeModal) return;
|
||||
|
||||
const toolId = event.currentTarget.dataset.toolid;
|
||||
activeModal = true;
|
||||
await showToolkitDetailsModal(toolId);
|
||||
activeModal = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -3503,11 +3509,15 @@ async function initializeToolboxTable(data, toolboxOwn, quantityMonitoring) {
|
||||
});
|
||||
|
||||
// Добавляем обработчики для изображений
|
||||
let activeModal = null;
|
||||
document.querySelectorAll('.toolkit-image-link').forEach(link => {
|
||||
link.addEventListener('click', async (e) => {
|
||||
if (activeModal) return;
|
||||
e.preventDefault();
|
||||
const itemId = e.currentTarget.dataset.id;
|
||||
activeModal = true;
|
||||
await showToolkitDetailsModal(itemId);
|
||||
activeModal = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -5371,7 +5381,7 @@ function renderRequestsTab(tabId, tabData) {
|
||||
<tr data-request-id="${request.id}">
|
||||
<td>
|
||||
<span class="badge bg-${request.action === 'Возврат' ? 'primary' : 'warning'}">${request.action}</span>
|
||||
<span class="text-muted">${request.created_at}</span>
|
||||
<span class="small text-muted">${request.created_at}</span>
|
||||
</td>
|
||||
<td>${userMap[request.init_user_id] || `Пользователь ${request.init_user_id}`}</td>
|
||||
<td>${request.source_toolbox_id ? (toolboxMap[request.source_toolbox_id] || `Склад ${request.source_toolbox_id}`) : '-'}</td>
|
||||
@@ -5851,8 +5861,8 @@ function renderJurnalToolkitsTab(tabId, tabData) {
|
||||
<span class="badge bg-${request.action === 'Возврат' ? 'primary' : 'warning'}">${request.action}</span>
|
||||
${statusBadge}
|
||||
</td>
|
||||
<td>${userMap[request.init_user_id] || `Пользователь ${request.init_user_id}`}<br><span class="text-muted">${request.created_at}</span></td>
|
||||
<td>${userMap[request.decision_user_id] || `Пользователь ${request.decision_user_id}`}<br><span class="text-muted">${request.decided_at}</span></td>
|
||||
<td>${userMap[request.init_user_id] || `Пользователь ${request.init_user_id}`}<br><span class="small text-muted">${request.created_at}</span></td>
|
||||
<td>${userMap[request.decision_user_id] || `Пользователь ${request.decision_user_id}`}<br><span class="small text-muted">${request.decided_at}</span></td>
|
||||
<td>${request.source_toolbox_id ? (toolboxMap[request.source_toolbox_id] || `Склад ${request.source_toolbox_id}`) : '-'}</td>
|
||||
<td>${request.target_toolbox_id ? (toolboxMap[request.target_toolbox_id] || `Склад ${request.target_toolbox_id}`) : '-'}</td>
|
||||
<td>${request.toolkit_id ? (toolkitMap[request.toolkit_id] || `Инструмент ${request.toolkit_id}`) : '-'}</td>
|
||||
@@ -5889,6 +5899,334 @@ function renderJurnalToolkitsTab(tabId, tabData) {
|
||||
// Первоначальный рендеринг таблицы
|
||||
renderRequestsTable();
|
||||
}
|
||||
|
||||
function renderJurnalServicesTab(tabId, tabData) {
|
||||
const tabContent = document.getElementById(`${tabId}-tab-content`);
|
||||
const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`);
|
||||
|
||||
const { requests, users, categories, startDate, endDate } = tabData;
|
||||
|
||||
if (requests.length === 0) {
|
||||
tabContent.innerHTML = `
|
||||
<div class="alert alert-info m-4" role="alert">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
Нет данных за период ${startDate} - ${endDate}
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Собираем списки для фильтров
|
||||
const initUsers = [...new Set(requests.map(r => r.user_id))].filter(id => id !== null);
|
||||
const userMap = {};
|
||||
users.forEach(user => {
|
||||
userMap[user.id] = user.username;
|
||||
});
|
||||
|
||||
const categoriesMap = {};
|
||||
categories.forEach(cat => {
|
||||
categoriesMap[cat.id] = { title: cat.title, description: cat.description };
|
||||
});
|
||||
|
||||
// Собираем типы действий (ключи из details)
|
||||
const actionTypes = [...new Set(requests.map(r => {
|
||||
const details = r.details;
|
||||
return Object.keys(details)[0]; // Берем первый ключ как тип действия
|
||||
}))];
|
||||
|
||||
const savedFilters = loadFromStorage(tabId);
|
||||
// Фильтры
|
||||
let currentFilters = {
|
||||
user: savedFilters?.user || 'all',
|
||||
action: savedFilters?.action || 'all'
|
||||
};
|
||||
|
||||
// Рендерим дополнительный контейнер с фильтрами
|
||||
tabOptionalContent.innerHTML = `
|
||||
<div class="row align-items-center mb-3">
|
||||
<!-- Фильтры слева -->
|
||||
<div class="col-12 col-md-8 mb-2 mb-md-0">
|
||||
<div class="row g-2 gap-1 mb-2">
|
||||
<div class="col-12 col-md-8">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-person"></i>
|
||||
</span>
|
||||
<select class="form-select" id="${tabId}-user-filter">
|
||||
<option value="all">Все пользователи</option>
|
||||
<option value="system">Система</option>
|
||||
${initUsers.map(userId => `
|
||||
<option value="${userId}">${userMap[userId] || `Пользователь ${userId}`}</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<button class="btn btn-outline-secondary" id="${tabId}-filter-reset-btn">
|
||||
<i class="bi bi-x-circle me-1"></i>Сброс
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-12 col-md-8">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-gear"></i>
|
||||
</span>
|
||||
<select class="form-select" id="${tabId}-action-filter">
|
||||
<option value="all">Все действия</option>
|
||||
${actionTypes.map(action => `
|
||||
<option value="${action}">${action}</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Фильтры справа -->
|
||||
<div class="col-12 col-md-4 pe-3">
|
||||
<div class="d-flex flex-wrap gap-2 justify-content-md-end">
|
||||
<div class="input-group date">
|
||||
<span class="input-group-text" style="width: 170px;">
|
||||
<i class="bi bi-calendar me-1"></i>Дата начала:
|
||||
</span>
|
||||
<input type="date" class="form-control" id="${tabId}-date-from">
|
||||
</div>
|
||||
<div class="input-group date">
|
||||
<span class="input-group-text" style="width: 170px;">
|
||||
<i class="bi bi-calendar me-1"></i>Дата окончания:
|
||||
</span>
|
||||
<input type="date" class="form-control" id="${tabId}-date-to">
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-outline-primary" id="${tabId}-date-update-btn">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>Обновить журнал
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const filterResetBtn = document.getElementById(`${tabId}-filter-reset-btn`);
|
||||
filterResetBtn.addEventListener('click', () => {
|
||||
currentFilters = {
|
||||
user: 'all',
|
||||
action: 'all'
|
||||
};
|
||||
document.getElementById(`${tabId}-user-filter`).value = currentFilters.user;
|
||||
document.getElementById(`${tabId}-action-filter`).value = currentFilters.action;
|
||||
saveToStorage(tabId, currentFilters);
|
||||
renderServicesTable();
|
||||
});
|
||||
|
||||
const startDateInput = document.getElementById(`${tabId}-date-from`);
|
||||
const endDateInput = document.getElementById(`${tabId}-date-to`);
|
||||
startDateInput.value = startDate;
|
||||
endDateInput.value = endDate;
|
||||
|
||||
const refreshDateBtn = document.getElementById(`${tabId}-date-update-btn`);
|
||||
refreshDateBtn.addEventListener('click', async () => {
|
||||
const newStartDate = startDateInput.value;
|
||||
const newEndDate = endDateInput.value;
|
||||
const newDateRequestData = {
|
||||
tabId: tabId,
|
||||
startDate: newStartDate,
|
||||
endDate: newEndDate
|
||||
};
|
||||
if (newStartDate && newEndDate) {
|
||||
tabContent.innerHTML = `
|
||||
<div class="alert alert-info m-4" role="alert">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
Загрузка данных...
|
||||
</div>
|
||||
`;
|
||||
const newPeriodData = await apiRequest('/', newDateRequestData);
|
||||
if (newPeriodData.status == 'ok') {
|
||||
renderJurnalServicesTab(tabId, newPeriodData.data);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Рендерим основной контейнер с таблицей сервисных событий
|
||||
tabContent.innerHTML = `
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0" id="${tabId}-services-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th width="110">Дата</th>
|
||||
<th width="200">Пользователь</th>
|
||||
<th width="200">Действие</th>
|
||||
<th>Детали</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="${tabId}-services-body">
|
||||
<!-- События будут вставлены здесь -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="text-center p-3 border-top" id="${tabId}-no-services" style="display: none;">
|
||||
<i class="bi bi-inbox fs-1 text-muted"></i>
|
||||
<p class="text-muted mt-2">Нет событий по выбранным фильтрам</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Функция для фильтрации событий
|
||||
function filterServices() {
|
||||
let filtered = requests;
|
||||
|
||||
// Фильтр по пользователю
|
||||
if (currentFilters.user !== 'all') {
|
||||
if (currentFilters.user === 'system') {
|
||||
filtered = filtered.filter(r => r.user_id === null);
|
||||
} else {
|
||||
filtered = filtered.filter(r => r.user_id == currentFilters.user);
|
||||
}
|
||||
}
|
||||
|
||||
// Фильтр по типу действия
|
||||
if (currentFilters.action !== 'all') {
|
||||
filtered = filtered.filter(r => {
|
||||
const details = r.details;
|
||||
return Object.keys(details)[0] === currentFilters.action;
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// Функция для рендеринга строк таблицы
|
||||
function renderServicesTable() {
|
||||
const tbody = document.getElementById(`${tabId}-services-body`);
|
||||
const noServicesDiv = document.getElementById(`${tabId}-no-services`);
|
||||
const filteredServices = filterServices();
|
||||
|
||||
if (filteredServices.length === 0) {
|
||||
tbody.innerHTML = '';
|
||||
noServicesDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
noServicesDiv.style.display = 'none';
|
||||
|
||||
tbody.innerHTML = filteredServices.map(service => {
|
||||
const actionType = Object.keys(service.details)[0];
|
||||
const actionData = service.details[actionType];
|
||||
|
||||
// Определяем пользователя
|
||||
let userName = 'Система';
|
||||
if (service.user_id) {
|
||||
userName = userMap[service.user_id] || `Пользователь ${service.user_id}`;
|
||||
}
|
||||
|
||||
// Форматируем детали в зависимости от типа действия
|
||||
let detailsHtml = '';
|
||||
|
||||
if (actionType === 'Авторизован пользователь') {
|
||||
// Для авторизации
|
||||
detailsHtml = `
|
||||
<div class="fw-semibold">${actionData}</div>
|
||||
`;
|
||||
} else if (actionType.includes('Добавлен') || actionType.includes('Обновлен') || actionType.includes('Добавлена')) {
|
||||
// Для добавления/обновления сущностей
|
||||
const entityName = actionData.title || actionData.username || actionData.login || '';
|
||||
detailsHtml = `
|
||||
<div class="fw-semibold">${entityName}</div>
|
||||
${actionData.description ? `<div class="text-muted small mt-1">${actionData.description}</div>` : ''}
|
||||
${actionData.id ? `<div class="text-muted small">ID: ${actionData.id}</div>` : ''}
|
||||
`;
|
||||
|
||||
// Для инструментов добавляем дополнительные поля
|
||||
if (actionData.specifications && Object.keys(actionData.specifications).length > 0) {
|
||||
detailsHtml += `<div class="mt-2"><strong>Характеристики:</strong></div>`;
|
||||
detailsHtml += `<div class="small text-muted">`;
|
||||
for (const [key, value] of Object.entries(actionData.specifications)) {
|
||||
detailsHtml += `<div>${key}: ${value}</div>`;
|
||||
}
|
||||
detailsHtml += `</div>`;
|
||||
}
|
||||
|
||||
if (actionData.external_link) {
|
||||
detailsHtml += `<div class="mt-2"><strong>Ссылка:</strong> <a href="${actionData.external_link}" target="_blank">${actionData.external_link}</a></div>`;
|
||||
}
|
||||
|
||||
if (actionData.quantity_min || actionData.quantity_min_extra) {
|
||||
detailsHtml += `<div class="mt-2"><strong>Мониторинг остатков:</strong></div>`;
|
||||
if (actionData.quantity_min && actionData.quantity_min_extra) {
|
||||
detailsHtml += `<div class="small text-muted">Минимальное количество: ${actionData.quantity_min}</div>`;
|
||||
detailsHtml += `<div class="small text-muted">Минимальное критическое количество: ${actionData.quantity_min_extra}</div>`;
|
||||
} else if (actionData.quantity_min && !actionData.quantity_min_extra) {
|
||||
detailsHtml += `<div class="small text-muted">Минимальное количество: ${actionData.quantity_min}</div>`;
|
||||
} else if (actionData.quantity_min_extra && !actionData.quantity_min) {
|
||||
detailsHtml += `<div class="small text-muted">Минимальное критическое количество: ${actionData.quantity_min_extra}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (actionData.category_id) {
|
||||
detailsHtml += `<div class="mt-2"><span class="fw-bold">Категория:</span> <span class="fw-medium">${categoriesMap[actionData.category_id].title}</span> [${categoriesMap[actionData.category_id].description}]</div>`;
|
||||
}
|
||||
|
||||
if (actionData.image) {
|
||||
detailsHtml += `<div class="mt-2, fw-bold">Изображения:</div>`;
|
||||
detailsHtml += `<div class="small text-muted">Основное:<div>`;
|
||||
detailsHtml += `<div class="mt-2"><img src="${actionData.image.main}" class="img-thumbnail" style="width: 64px; height: 64px;" alt="Основное изображение инструмента">`;
|
||||
if (actionData.image.additional) {
|
||||
detailsHtml += `<div class="small text-muted">Дополнительные:<div>`;
|
||||
detailsHtml += `<div class="d-flex mt-2">`;
|
||||
actionData.image.additional.forEach(img => {
|
||||
detailsHtml += `<div><img src="${img}" class="img-thumbnail" style="width: 64px; height: 64px;" alt="Дополнительное изображение инструмента"></div>`;
|
||||
});
|
||||
detailsHtml += `</div>`;
|
||||
}
|
||||
detailsHtml += `</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<tr data-service-id="${service.id}">
|
||||
<td>
|
||||
<span class="text-muted">${service.created_at}</span>
|
||||
</td>
|
||||
<td>${userName}</td>
|
||||
<td>
|
||||
<span class="badge bg-info">${actionType}</span>
|
||||
</td>
|
||||
<td>
|
||||
${detailsHtml}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Назначаем обработчики событий для фильтров
|
||||
document.getElementById(`${tabId}-user-filter`).addEventListener('change', function () {
|
||||
currentFilters.user = this.value;
|
||||
saveToStorage(tabId, currentFilters);
|
||||
renderServicesTable();
|
||||
});
|
||||
|
||||
document.getElementById(`${tabId}-action-filter`).addEventListener('change', function () {
|
||||
currentFilters.action = this.value;
|
||||
saveToStorage(tabId, currentFilters);
|
||||
renderServicesTable();
|
||||
});
|
||||
|
||||
// Устанавливаем сохраненные значения фильтров
|
||||
if (savedFilters) {
|
||||
document.getElementById(`${tabId}-user-filter`).value = currentFilters.user;
|
||||
document.getElementById(`${tabId}-action-filter`).value = currentFilters.action;
|
||||
}
|
||||
|
||||
// Первоначальный рендеринг таблицы
|
||||
renderServicesTable();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await getCookieData();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user