сервисный журнал, фикс мультиклика на инструменты

This commit is contained in:
2025-12-14 11:52:48 +03:00
parent a3c48b55a1
commit ccec507033
9 changed files with 415 additions and 10 deletions
+342 -4
View File
@@ -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();