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

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
+38 -3
View File
@@ -114,6 +114,9 @@ async def post_requests(
if isinstance(endDate, str): if isinstance(endDate, str):
endDate = datetime.strptime(endDate, "%Y-%m-%d").date() endDate = datetime.strptime(endDate, "%Y-%m-%d").date()
if startDate > endDate:
startDate, endDate = endDate, startDate
jurnal_toolkits = await StocksRecordsHandler.getLogs(startDate, endDate) jurnal_toolkits = await StocksRecordsHandler.getLogs(startDate, endDate)
if isinstance(jurnal_toolkits, list): if isinstance(jurnal_toolkits, list):
if len(jurnal_toolkits) == 0: if len(jurnal_toolkits) == 0:
@@ -143,10 +146,42 @@ async def post_requests(
"endDate": endDate.strftime("%Y-%m-%d"), "endDate": endDate.strftime("%Y-%m-%d"),
} }
case "jurnal_service": case "jurnal_service":
jurnal_service = await ServiceRecordsHandler.get() startDate = request_data.get("body").get(
if jurnal_service: "startDate", date.today() - timedelta(days=7)
)
if isinstance(startDate, str):
startDate = datetime.strptime(startDate, "%Y-%m-%d").date()
endDate = request_data.get("body").get("endDate", date.today())
if isinstance(endDate, str):
endDate = datetime.strptime(endDate, "%Y-%m-%d").date()
if startDate > endDate:
startDate, endDate = endDate, startDate
jurnal_service = await ServiceRecordsHandler.getLogs(startDate, endDate)
if isinstance(jurnal_service, list):
if len(jurnal_service) == 0:
resultData["status"] = "ok" resultData["status"] = "ok"
resultData["data"] = jurnal_service resultData["data"] = {
"requests": [],
"users": [],
"categories": [],
"startDate": startDate.strftime("%Y-%m-%d"),
"endDate": endDate.strftime("%Y-%m-%d"),
}
else:
users = await UserHandler.getAll()
categories = await CategoryHandler.getAll()
resultData["status"] = "ok"
resultData["data"] = {
"requests": jurnal_service,
"users": users,
"categories": categories,
"startDate": startDate.strftime("%Y-%m-%d"),
"endDate": endDate.strftime("%Y-%m-%d"),
}
# logger.info(resultData.get("data"))
case "users": case "users":
users = await UserHandler.getAll() users = await UserHandler.getAll()
if users: if users:
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -160,7 +160,7 @@ async def manage_toolkit(reqData: dict = Depends(requestDict)):
logger.info(f"Управление инструментами") logger.info(f"Управление инструментами")
response = {"status": "error"} response = {"status": "error"}
action = reqData.get("body").get("action") action = reqData.get("body").get("action")
userId = reqData.get("body").get("UserId") userId = reqData.get("body").get("userId")
toolkitData = reqData.get("body").get("formData") toolkitData = reqData.get("body").get("formData")
if "category_id" in toolkitData: if "category_id" in toolkitData:
toolkitData["category_id"] = int(toolkitData.get("category_id")) toolkitData["category_id"] = int(toolkitData.get("category_id"))
+342 -4
View File
@@ -264,7 +264,7 @@ function fillTab(tabId, tabData) {
renderJurnalToolkitsTab(tabId, tabData); renderJurnalToolkitsTab(tabId, tabData);
break; break;
case 'jurnal_service': case 'jurnal_service':
renderSimpleTab(tabId, tabData, 'Сервисный журнал'); renderJurnalServicesTab(tabId, tabData);
break; break;
case 'users': case 'users':
renderSimpleTab(tabId, tabData, 'Пользователи системы'); renderSimpleTab(tabId, tabData, 'Пользователи системы');
@@ -1454,10 +1454,16 @@ function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', catego
}).join(''); }).join('');
const cards = container.querySelectorAll('.toolkit-card'); const cards = container.querySelectorAll('.toolkit-card');
let activeModal = null;
cards.forEach(card => { cards.forEach(card => {
card.addEventListener('click', async event => { card.addEventListener('click', async event => {
if (activeModal) return;
const toolId = event.currentTarget.dataset.toolid; const toolId = event.currentTarget.dataset.toolid;
activeModal = true;
await showToolkitDetailsModal(toolId); 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 => { document.querySelectorAll('.toolkit-image-link').forEach(link => {
link.addEventListener('click', async (e) => { link.addEventListener('click', async (e) => {
if (activeModal) return;
e.preventDefault(); e.preventDefault();
const itemId = e.currentTarget.dataset.id; const itemId = e.currentTarget.dataset.id;
activeModal = true;
await showToolkitDetailsModal(itemId); await showToolkitDetailsModal(itemId);
activeModal = null;
}); });
}); });
} }
@@ -5371,7 +5381,7 @@ function renderRequestsTab(tabId, tabData) {
<tr data-request-id="${request.id}"> <tr data-request-id="${request.id}">
<td> <td>
<span class="badge bg-${request.action === 'Возврат' ? 'primary' : 'warning'}">${request.action}</span> <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>
<td>${userMap[request.init_user_id] || `Пользователь ${request.init_user_id}`}</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> <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> <span class="badge bg-${request.action === 'Возврат' ? 'primary' : 'warning'}">${request.action}</span>
${statusBadge} ${statusBadge}
</td> </td>
<td>${userMap[request.init_user_id] || `Пользователь ${request.init_user_id}`}<br><span class="text-muted">${request.created_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="text-muted">${request.decided_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.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.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> <td>${request.toolkit_id ? (toolkitMap[request.toolkit_id] || `Инструмент ${request.toolkit_id}`) : '-'}</td>
@@ -5889,6 +5899,334 @@ function renderJurnalToolkitsTab(tabId, tabData) {
// Первоначальный рендеринг таблицы // Первоначальный рендеринг таблицы
renderRequestsTable(); 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 () => { document.addEventListener('DOMContentLoaded', async () => {
await getCookieData(); await getCookieData();
Binary file not shown.
Binary file not shown.
+32
View File
@@ -287,3 +287,35 @@ class ServiceRecordsHandler:
except Exception as e: except Exception as e:
logger.error(f"Ошибка получения записей: {str(e)}") logger.error(f"Ошибка получения записей: {str(e)}")
return False return False
async def getLogs(startDate: date, endDate: date):
from db import CRUD
try:
start_dt = datetime.combine(startDate, time.min)
end_dt = datetime.combine(endDate, time.max)
query = (
select(ServicesRecords)
.where(
ServicesRecords.created_at.between(start_dt, end_dt),
)
.order_by(ServicesRecords.created_at.desc())
)
logger.debug("Получение записей за период %s - %s", startDate, endDate)
records = await CRUD.read(query, True)
logger.debug(
"%d записей за период %s - %s успешно получены",
len(records),
startDate,
endDate,
)
return [record.toDict() for record in records]
except Exception:
logger.exception("Ошибка получения записей")
return []
+1 -1
View File
@@ -160,7 +160,7 @@ class ToolkitHandler:
f"Инструмент {editedToolkit.title} успешно обновлен, изменены данные: {kwargs.keys()}" f"Инструмент {editedToolkit.title} успешно обновлен, изменены данные: {kwargs.keys()}"
) )
await ServiceRecordsHandler.add( await ServiceRecordsHandler.add(
user_id, {f"Обновлен инструмент {toolkit.title}": editedToolkit.toDict()} user_id, {f"Обновлен инструмент": editedToolkit.toDict()}
) )
return editedToolkit.toDict() return editedToolkit.toDict()