заказы

This commit is contained in:
2025-12-21 03:45:02 +03:00
parent 83ecf65abf
commit 546c70cbcd
15 changed files with 862 additions and 38 deletions
+1
View File
@@ -2,6 +2,7 @@ body {
background-image: url("../images/background.svg");
background-repeat: repeat;
background-size: 512px auto;
background-attachment: fixed;
}
.loader-bg {
+621 -7
View File
@@ -89,7 +89,11 @@ async function checkActiveUser() {
}
}
async function openTab(event, tabId) {
async function openTab(event, tabId, autoLoad = false) {
const activeTab = loadFromStorage('tab');
if (activeTab && activeTab.tabId === tabId && !autoLoad) {
return;
}
// Убираем активный класс со всех вкладок и кнопок
document.querySelectorAll('.tab-nav-btn').forEach(btn => {
btn.classList.remove('active');
@@ -136,6 +140,12 @@ function prepareTabs() {
};
}
tabsData['orders'] = {
title: 'Заказы',
icon: 'bi-basket',
description: 'Управление заказами'
};
if (accessData.view_requests) {
tabsData['jurnal_toolkits'] = {
title: 'Журнал перемещений',
@@ -170,11 +180,11 @@ function prepareTabs() {
<div id="mainTabsNavWrapper">
<nav class="nav nav-pills gap-2" id="mainTabsNav" role="tablist">
${Object.entries(tabsData).map(([tabId, tabData], index) => `
<button class="nav-link tab-nav-btn d-flex flex-column align-items-center justify-content-center py-3 px-2"
id="${tabId}-tab"
role="tab"
onclick="openTab(event, '${tabId}')"
style="min-width: 120px; transition: all 0.3s ease;">
<button class="nav-link tab-nav-btn d-flex flex-column align-items-center justify-content-center py-3 px-2 position-relative"
id="${tabId}-tab"
role="tab"
onclick="openTab(event, '${tabId}')"
style="min-width: 120px; transition: all 0.3s ease;">
<i class="${tabData.icon} nav-icon fs-3 mb-2 text-muted" style="transition: all 0.3s ease;"></i>
<span class="nav-title fw-medium" style="font-size: 0.9rem;">${tabData.title}</span>
<div class="nav-indicator"></div>
@@ -240,7 +250,7 @@ function prepareTabs() {
const activeTabId = activeTabData.tabId;
const tabBtn = document.getElementById(`${activeTabId}-tab`);
if (tabBtn) {
openTab({ currentTarget: tabBtn }, activeTabId);
openTab({ currentTarget: tabBtn }, activeTabId, true);
}
}
}
@@ -280,6 +290,9 @@ function fillTab(tabId, tabData) {
case 'toolkits':
renderToolkitsTab(tabId, tabData.toolkits, tabData.categories);
break;
case 'orders':
renderOrdersTab(tabId, tabData);
break;
case 'jurnal_toolkits':
renderJurnalToolkitsTab(tabId, tabData);
break;
@@ -5986,6 +5999,578 @@ function renderRequestsTab(tabId, tabData) {
renderRequestsTable();
}
function renderOrdersTab(tabId, tabData) {
const tabContent = document.getElementById(`${tabId}-tab-content`);
const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`);
const { orders, users, startDate, endDate, fullAccess } = tabData;
// Создаем мапу пользователей
const userMap = {};
users.forEach(user => {
userMap[user.id] = user.username;
});
// const ordersStatuses = {
// }
// Сохраненные фильтры
const savedFilters = loadFromStorage(tabId);
let currentFilters = {
customer: savedFilters?.customer || 'all',
status: savedFilters?.status || 'all',
search: savedFilters?.search || ''
};
// Хранилище измененных данных
const changedOrders = {};
const statusesList = []
const statusesMap = {
'new': 'Новый',
'working': 'В работе',
'complete': 'Выполнен',
'cancelled': 'Отменен',
}
// Оригинальные данные заказов (для сравнения)
const originalOrders = {};
orders.forEach(o => {
originalOrders[o.id] = {
status: o.status,
executor_comment: o.executor_comment || ''
};
if (!statusesList.includes(o.status)) {
statusesList.push(o.status)
}
});
// Рендерим дополнительный контейнер с фильтрами и кнопками
tabOptionalContent.innerHTML = `
<div class="row align-items-center mb-1">
<!-- Левая часть с фильтрами -->
<div class="col-12 col-md-6 mb-2 mb-md-0">
<div class="row align-items-center mb-2">
<!-- Поиск -->
<div class="col-12 col-md-9">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-search"></i>
</span>
<input type="text" class="form-control"
id="${tabId}-search-filter"
placeholder="Поиск по таблице..."
value="${currentFilters.search}">
</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-5">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-list-check"></i>
</span>
<select class="form-select" id="${tabId}-status-filter">
<option value="all">Все статусы</option>
${statusesList.map(status => `<option value="${status}">${statusesMap[status] || status}</option>`).join('')}
</select>
</div>
</div>
${!fullAccess ? `
<div class="col-12 col-md-7">
<button class="btn btn-primary" id="${tabId}-new-order-btn">
<i class="bi bi-plus-circle me-1"></i>Оформить заказ
</button>
</div>
` : `
<div class="col-12 col-md-7">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-person"></i>
</span>
<select class="form-select" id="${tabId}-customer-filter">
<option value="all">Все заказчики</option>
${[...new Set(orders.map(o => o.customer_id))].map(customerId => `
<option value="${customerId}">${userMap[customerId] || `Пользователь ${customerId}`}</option>
`).join('')}
</select>
</div>
</div>
` }
</div>
</div>
<div class="col-12 col-md-2">
<button class="btn btn-outline-primary" id="${tabId}-date-update-btn">
Обновить список заказов
</button>
</div>
<!-- Правая часть с датами и кнопками -->
<div class="col-12 col-md-4">
<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>
</div>
</div>
`;
// Модальное окно для нового заказа
if (!fullAccess) {
document.body.insertAdjacentHTML('beforeend', `
<div class="modal fade" id="${tabId}-new-order-modal" tabindex="-1">
<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"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="${tabId}-order-description" class="form-label">Описание заказа</label>
<textarea class="form-control" id="${tabId}-order-description"
rows="4" placeholder="Опишите, что вам нужно..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" id="${tabId}-order-submit-btn">Заказать</button>
</div>
</div>
</div>
</div>
`);
}
// Инициализация фильтров
if (fullAccess) {
document.getElementById(`${tabId}-customer-filter`).value = currentFilters.customer;
}
document.getElementById(`${tabId}-status-filter`).value = currentFilters.status;
document.getElementById(`${tabId}-search-filter`).value = currentFilters.search;
// Обработчики событий
const filterResetBtn = document.getElementById(`${tabId}-filter-reset-btn`);
filterResetBtn.addEventListener('click', () => {
currentFilters = {
customer: 'all',
status: 'all',
search: ''
};
if (fullAccess) {
document.getElementById(`${tabId}-customer-filter`).value = currentFilters.customer;
}
document.getElementById(`${tabId}-status-filter`).value = currentFilters.status;
document.getElementById(`${tabId}-search-filter`).value = currentFilters.search;
saveToStorage(tabId, currentFilters);
renderOrdersTable();
});
// Дата фильтры
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;
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 cookiesData = { userData, accessData };
const newPeriodData = await apiRequest('/', {
tabId: tabId,
startDate: newStartDate,
endDate: newEndDate,
cookiesData
});
if (newPeriodData.status === 'ok') {
renderOrdersTab(tabId, {
...tabData,
...newPeriodData.data,
startDate: newStartDate,
endDate: newEndDate,
fullAccess
});
await checkNewOrders();
}
}
});
// Кнопка нового заказа
if (!fullAccess) {
const newOrderBtn = document.getElementById(`${tabId}-new-order-btn`);
newOrderBtn.addEventListener('click', () => {
const modal = new bootstrap.Modal(document.getElementById(`${tabId}-new-order-modal`));
modal.show();
});
// Отправка нового заказа
const orderSubmitBtn = document.getElementById(`${tabId}-order-submit-btn`);
orderSubmitBtn.onclick = async () => {
if (orderSubmitBtn.disabled) return;
orderSubmitBtn.disabled = true;
const description = document.getElementById(`${tabId}-order-description`).value;
if (!description.trim()) {
showInfo('Пожалуйста, введите описание заказа', 'warning');
orderSubmitBtn.disabled = false;
return;
}
try {
const result = await apiRequest('/orders/new', {
userId: userData.id,
customer_comment: description
});
if (result.status === 'ok') {
const modal = bootstrap.Modal.getInstance(
document.getElementById(`${tabId}-new-order-modal`)
);
modal.hide();
document.getElementById(`${tabId}-order-description`).value = '';
refreshDateBtn.click();
} else {
showInfo(result.message || 'Ошибка при создании заказа', 'danger');
}
} finally {
orderSubmitBtn.disabled = false;
}
};
}
if (orders.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;
}
// Рендерим таблицу
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}-orders-table">
<thead class="table-light">
<tr>
<th width="150">Заказчик</th>
<th>Описание заказа</th>
<th width="120">Статус</th>
<th width="150">Исполнитель</th>
<th>Комментарий исполнителя</th>
${fullAccess ? '<th width="30">⚙️</th>' : ''}
</tr>
</thead>
<tbody id="${tabId}-orders-body">
<!-- Заказы будут вставлены здесь -->
</tbody>
</table>
</div>
<div class="text-center p-3 border-top" id="${tabId}-no-orders" style="display: none;">
<i class="bi bi-inbox fs-1 text-muted"></i>
<p class="text-muted mt-2">Нет заказов по выбранным фильтрам</p>
</div>
</div>
</div>
`;
// Функция фильтрации
function filterOrders() {
let filtered = orders;
// Фильтр по заказчику
if (fullAccess && currentFilters.customer !== 'all') {
filtered = filtered.filter(o => o.customer_id == currentFilters.customer);
}
// Фильтр по статусу
if (currentFilters.status !== 'all') {
filtered = filtered.filter(o => o.status === currentFilters.status);
}
// Поиск
if (currentFilters.search.trim()) {
const searchTerm = currentFilters.search.toLowerCase();
filtered = filtered.filter(o => {
return (
(userMap[o.customer_id] || '').toLowerCase().includes(searchTerm) ||
o.customer_comment.toLowerCase().includes(searchTerm) ||
(userMap[o.executor_id] || '').toLowerCase().includes(searchTerm) ||
(o.executor_comment || '').toLowerCase().includes(searchTerm) ||
o.status.toLowerCase().includes(searchTerm)
);
});
}
return filtered;
}
// Функция рендеринга таблицы
function renderOrdersTable() {
const tbody = document.getElementById(`${tabId}-orders-body`);
const noOrdersDiv = document.getElementById(`${tabId}-no-orders`);
const filteredOrders = filterOrders();
if (filteredOrders.length === 0) {
tbody.innerHTML = '';
noOrdersDiv.style.display = 'block';
return;
}
noOrdersDiv.style.display = 'none';
tbody.innerHTML = filteredOrders.map(order => {
// Определяем класс для статуса
const statusClass = {
new: 'warning',
working: 'primary',
complete: 'success',
cancelled: 'danger'
}[order.status] || 'secondary';
// Определяем русское название статуса
const statusText = {
new: 'Новый',
working: 'В работе',
complete: 'Выполнен',
cancelled: 'Отменен'
}[order.status] || order.status;
// Рендерим статус
let statusCell;
if (fullAccess && (order.status === 'new' || order.status === 'working')) {
statusCell = `
<select class="form-select form-select-sm order-status"
data-order-id="${order.id}"
style="min-width: 120px;">
<option value="new" ${order.status === 'new' ? 'selected' : ''} disabled>Новый</option>
<option value="working" ${order.status === 'working' ? 'selected' : ''}>В работе</option>
<option value="complete" ${order.status === 'complete' ? 'selected' : ''}>Выполнен</option>
<option value="cancelled" ${order.status === 'cancelled' ? 'selected' : ''}>Отменен</option>
</select>
`;
} else {
statusCell = `<span class="badge bg-${statusClass}">${statusText}</span>`;
}
// Рендерим комментарий исполнителя
let commentCell;
if (fullAccess && (order.status === 'new' || order.status === 'working')) {
commentCell = `
<textarea class="form-control form-control-sm executor-comment"
data-order-id="${order.id}"
rows="2"
placeholder="Введите комментарий...">${order.executor_comment || ''}</textarea>
`;
} else {
commentCell = `<small class="text-muted">${order.executor_comment || 'Нет комментария'}</small>`;
}
return `
<tr data-order-id="${order.id}">
<td class="align-middle">
${userMap[order.customer_id] || `Пользователь ${order.customer_id}`}<br>
<small class="text-muted">${order.created_at}</small>
</td>
<td class="align-middle"><small>${order.customer_comment}</small></td>
<td class="align-middle">
${statusCell}<br>
<small class="text-muted">${order.updated_at}</small>
</td>
<td class="align-middle">${order.executor_id ? (userMap[order.executor_id] || `Пользователь ${order.executor_id}`) : '-'}</td>
<td class="align-middle">${commentCell}</td>
${fullAccess ? `
<td class="align-middle text-center">
${fullAccess && (order.status === 'new' || order.status === 'working') ? `
<button class="btn btn-sm btn-success save-row-btn"
data-order-id="${order.id}"
disabled>
<i class="bi bi-save"></i>
</button>
` : '☑️'}
</td>
`: ''}
</tr>
`;
}).join('');
function updateRowState(orderId) {
const original = originalOrders[orderId];
const current = changedOrders[orderId];
const hasChanges =
current &&
(
(current.status !== undefined && current.status !== original.status) ||
(current.executor_comment !== undefined && current.executor_comment !== original.executor_comment)
);
const saveBtn = document.querySelector(
`.save-row-btn[data-order-id="${orderId}"]`
);
if (saveBtn) {
saveBtn.disabled = !hasChanges;
}
}
// Добавляем обработчики изменений
if (fullAccess) {
document.querySelectorAll(`.order-status`).forEach(select => {
select.addEventListener('change', function () {
const orderId = this.dataset.orderId;
changedOrders[orderId] = {
...changedOrders[orderId],
status: this.value
};
updateRowState(orderId);
});
});
document.querySelectorAll(`.executor-comment`).forEach(textarea => {
textarea.addEventListener('input', function () {
const orderId = this.dataset.orderId;
changedOrders[orderId] = {
...changedOrders[orderId],
executor_comment: this.value
};
updateRowState(orderId);
});
});
document.querySelectorAll('.save-row-btn').forEach(btn => {
btn.addEventListener('click', async function () {
const orderId = this.dataset.orderId;
const original = originalOrders[orderId];
const current = changedOrders[orderId];
if (!current) return;
// Формируем diff
const payload = { orderId, userId: userData.id };
if (current.status !== undefined && current.status !== original.status) {
payload.status = current.status;
}
if (
current.executor_comment !== undefined &&
current.executor_comment !== original.executor_comment
) {
payload.comment = current.executor_comment;
}
if (Object.keys(payload).length === 2) return;
this.disabled = true;
const result = await apiRequest('/orders/', payload);
if (result.status === 'ok') {
// Обновляем оригинал
originalOrders[orderId] = {
status: payload.status ?? original.status,
executor_comment: payload.comment ?? original.executor_comment
};
delete changedOrders[orderId];
if (payload.status && original.status === 'new') {
await checkNewOrders();
}
this.innerHTML = '<i class="bi bi-check-circle"></i>';
setTimeout(() => {
this.innerHTML = '<i class="bi bi-save"></i>';
}, 1500);
if (originalOrders[orderId].status === 'complete' || originalOrders[orderId].status === 'cancelled') {
const foundOrder = orders.find(order => order.id === Number(orderId));
if (foundOrder) {
foundOrder.executor_id = userData.id;
foundOrder.executor_comment = originalOrders[orderId].executor_comment;
foundOrder.status = current.status;
}
renderOrdersTable();
}
} else {
showInfo('Ошибка сохранения', 'danger');
this.disabled = false;
}
});
});
}
}
// Обработчики фильтров
if (fullAccess) {
document.getElementById(`${tabId}-customer-filter`).addEventListener('change', function () {
currentFilters.customer = this.value;
saveToStorage(tabId, currentFilters);
renderOrdersTable();
});
}
document.getElementById(`${tabId}-status-filter`).addEventListener('change', function () {
currentFilters.status = this.value;
saveToStorage(tabId, currentFilters);
renderOrdersTable();
});
document.getElementById(`${tabId}-search-filter`).addEventListener('input', function () {
currentFilters.search = this.value;
saveToStorage(tabId, currentFilters);
renderOrdersTable();
});
// Первоначальный рендеринг
renderOrdersTable();
}
function renderJurnalToolkitsTab(tabId, tabData) {
const tabContent = document.getElementById(`${tabId}-tab-content`);
const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`);
@@ -7446,6 +8031,34 @@ function renderUsersTab(tabId, tabData) {
renderUsersCards();
}
async function checkNewOrders() {
const result = await apiRequest('/orders/', {}, 'GET');
if (result && result.orders > 0) {
const ordersTabBtn = document.getElementById('orders-tab');
// Удаляем старый бейдж, если есть
const oldBadge = ordersTabBtn.querySelector('.orders-badge');
if (oldBadge) oldBadge.remove();
const newOrdersBadge = document.createElement('span');
newOrdersBadge.className =
'badge rounded-pill bg-danger position-absolute orders-badge';
newOrdersBadge.textContent = result.orders;
// Позиция: правый верх
newOrdersBadge.style.top = '6px';
newOrdersBadge.style.right = '8px';
ordersTabBtn.appendChild(newOrdersBadge);
}
if (result && result.orders == 0) {
const ordersTabBtn = document.getElementById('orders-tab');
const oldBadge = ordersTabBtn.querySelector('.orders-badge');
if (oldBadge) oldBadge.remove();
}
}
document.addEventListener('DOMContentLoaded', async () => {
await getCookieData();
@@ -7456,6 +8069,7 @@ document.addEventListener('DOMContentLoaded', async () => {
}
prepareTabs();
await checkNewOrders();
});
window.openTab = openTab;