Завершена работа со складами
This commit is contained in:
+500
-5
@@ -879,8 +879,8 @@ async function loadToolboxContent(toolboxId) {
|
||||
|
||||
function handleFillBtn() {
|
||||
if (accessData.tools_registration && !toolboxInfo.owner_id) {
|
||||
contentContainer.querySelector('#fillToolbox').addEventListener('click', () => {
|
||||
fillToolbox(toolboxInfo);
|
||||
contentContainer.querySelector('#fillToolbox').addEventListener('click', async () => {
|
||||
await fillToolbox(toolboxInfo);
|
||||
});
|
||||
} else {
|
||||
contentContainer.querySelector('#fillToolbox').remove();
|
||||
@@ -1047,9 +1047,504 @@ async function loadToolboxContent(toolboxId) {
|
||||
}
|
||||
}
|
||||
|
||||
function fillToolbox(toolboxInfo) {
|
||||
console.log(toolboxInfo);
|
||||
showInfo('Функционал еще в разработке', 'warning');
|
||||
async function fillToolbox(toolboxInfo) {
|
||||
const allToolkitsData = await apiRequest('/toolkit/fill_prepare');
|
||||
|
||||
if (allToolkitsData.status !== 'ok') {
|
||||
showInfo('Ошибка загрузки данных инструментов', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, существует ли уже модальное окно
|
||||
let modal = document.getElementById('fillToolboxModal');
|
||||
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
}
|
||||
|
||||
// Создаем модальное окно
|
||||
modal = document.createElement('div');
|
||||
modal.className = 'modal fade';
|
||||
modal.id = 'fillToolboxModal';
|
||||
modal.tabIndex = -1;
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="modal-dialog modal-dialog-centered modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-cart-plus-fill me-2"></i>
|
||||
Пополнение склада: ${toolboxInfo.title}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<form id="fillToolboxForm" novalidate>
|
||||
<div class="modal-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0" id="fillToolboxTable">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th style="width: 20%;">Категория</th>
|
||||
<th style="width: 25%;">Инструмент</th>
|
||||
<th style="width: 10%;">Количество</th>
|
||||
<th style="width: 12%;">Цена, ₽</th>
|
||||
<th style="width: 15%;">Расположение</th>
|
||||
<th style="width: 12%;">Стоимость, ₽</th>
|
||||
<th style="width: 6%;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="fillToolboxRows">
|
||||
<!-- Строки будут добавляться здесь -->
|
||||
</tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr>
|
||||
<td colspan="2" class="text-end fw-bold">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addRowBtn">
|
||||
<i class="bi bi-plus-circle me-1"></i>Добавить строку
|
||||
</button>
|
||||
</td>
|
||||
<td class="fw-bold text-center" id="totalQuantity">0</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="fw-bold text-center" id="totalCost">0.00</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="text-end">Итого:</td>
|
||||
<td class="text-center fw-bold" id="totalRowsCount">0 позиций</td>
|
||||
<td colspan="4"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="7" class="p-3">
|
||||
<div class="mb-3">
|
||||
<label for="fillReason" class="form-label required">Обоснование пополнения</label>
|
||||
<textarea class="form-control" id="fillReason" rows="1"
|
||||
placeholder="Укажите основание пополнения склада (например, накладная, счёт-фактура и т.д.)"
|
||||
required minlength="10" maxlength="500"></textarea>
|
||||
<div class="invalid-feedback">
|
||||
Пожалуйста, укажите обоснование (не менее 10 символов, не более 500)
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div id="fillToolboxError" class="alert alert-danger d-none w-100 mb-3" role="alert">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<span id="fillToolboxErrorMessage"></span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="submit" class="btn btn-success" id="submitFillBtn">
|
||||
<span class="spinner-border spinner-border-sm me-1"
|
||||
id="submitFillSpinner" style="display: none;"></span>
|
||||
<span id="submitFillText">Подтвердить пополнение</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Инициализация модального окна
|
||||
const bsModal = new bootstrap.Modal(modal);
|
||||
|
||||
// Получаем данные
|
||||
const { toolkits, categories, placements } = allToolkitsData.data;
|
||||
const placementMap = {};
|
||||
|
||||
// Создаем карту расположений для текущего склада
|
||||
placements
|
||||
.filter(p => p.toolbox_id === toolboxInfo.id)
|
||||
.forEach(p => {
|
||||
placementMap[p.toolkit_id] = p.placement;
|
||||
});
|
||||
|
||||
// Создаем карту инструментов по категориям
|
||||
const toolkitsByCategory = {};
|
||||
toolkits.forEach(tool => {
|
||||
if (!toolkitsByCategory[tool.category_id]) {
|
||||
toolkitsByCategory[tool.category_id] = [];
|
||||
}
|
||||
toolkitsByCategory[tool.category_id].push(tool);
|
||||
});
|
||||
|
||||
// Функция для форматирования стоимости с разделителями
|
||||
function formatCost(value) {
|
||||
if (typeof value !== 'number') {
|
||||
value = parseFloat(value) || 0;
|
||||
}
|
||||
return formatPrice(value);
|
||||
}
|
||||
|
||||
// Функция для создания строки формы
|
||||
function createRow(rowIndex = 0) {
|
||||
const rowId = `row-${Date.now()}-${rowIndex}`;
|
||||
return `
|
||||
<tr id="${rowId}" data-row-id="${rowId}">
|
||||
<td>
|
||||
<select class="form-select form-select-sm category-select"
|
||||
data-row="${rowId}" required>
|
||||
<option value="">Выберите категорию</option>
|
||||
${categories.map(cat =>
|
||||
`<option value="${cat.id}">${cat.title}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm toolkit-select"
|
||||
data-row="${rowId}" disabled required>
|
||||
<option value="">Выберите инструмент</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm quantity"
|
||||
data-row="${rowId}" min="1" step="1"
|
||||
placeholder="0" disabled required>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm price"
|
||||
data-row="${rowId}" min="0.01" step="0.01"
|
||||
placeholder="0.00" disabled required>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm placement"
|
||||
data-row="${rowId}" placeholder="Не указано" disabled>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm cost"
|
||||
data-row="${rowId}" value="0.00 ₽" readonly>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger remove-row"
|
||||
data-row="${rowId}" ${rowIndex === 0 ? 'disabled' : ''}>
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
// Инициализация первой строки
|
||||
const rowsContainer = document.getElementById('fillToolboxRows');
|
||||
rowsContainer.innerHTML = createRow(0);
|
||||
|
||||
// Добавляем обработчики событий
|
||||
setupEventHandlers();
|
||||
|
||||
// Функция для обновления итогов
|
||||
function updateTotals() {
|
||||
const rows = rowsContainer.querySelectorAll('tr');
|
||||
let totalQuantity = 0;
|
||||
let totalCost = 0;
|
||||
let filledRows = 0;
|
||||
|
||||
rows.forEach(row => {
|
||||
const quantityInput = row.querySelector('.quantity');
|
||||
const costInput = row.querySelector('.cost');
|
||||
|
||||
if (quantityInput && costInput &&
|
||||
quantityInput.value && costInput.value) {
|
||||
const quantity = parseInt(quantityInput.value) || 0;
|
||||
const cost = parseFloat(costInput.value.replace(/[^0-9.,]/g, '').replace(',', '.')) || 0;
|
||||
|
||||
totalQuantity += quantity;
|
||||
totalCost += cost;
|
||||
filledRows++;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('totalQuantity').textContent = totalQuantity;
|
||||
document.getElementById('totalCost').textContent = formatCost(totalCost) + ' ₽';
|
||||
document.getElementById('totalRowsCount').textContent =
|
||||
`${filledRows} позиций`;
|
||||
}
|
||||
|
||||
// Функция для расчета стоимости строки
|
||||
function calculateRowCost(row) {
|
||||
const quantityInput = row.querySelector('.quantity');
|
||||
const priceInput = row.querySelector('.price');
|
||||
const costInput = row.querySelector('.cost');
|
||||
|
||||
if (!quantityInput || !priceInput || !costInput) return;
|
||||
|
||||
const quantity = parseFloat(quantityInput.value) || 0;
|
||||
const price = parseFloat(priceInput.value.replace(',', '.')) || 0;
|
||||
const cost = quantity * price;
|
||||
|
||||
costInput.value = formatCost(cost) + ' ₽';
|
||||
updateTotals();
|
||||
}
|
||||
|
||||
// Функция для настройки обработчиков событий
|
||||
function setupEventHandlers() {
|
||||
// Обработчик добавления строки
|
||||
document.getElementById('addRowBtn').addEventListener('click', function () {
|
||||
const newRow = createRow(rowsContainer.children.length);
|
||||
rowsContainer.insertAdjacentHTML('beforeend', newRow);
|
||||
|
||||
// Обновляем доступность кнопок удаления
|
||||
const removeButtons = rowsContainer.querySelectorAll('.remove-row');
|
||||
if (removeButtons.length > 1) {
|
||||
removeButtons.forEach(btn => btn.disabled = false);
|
||||
}
|
||||
|
||||
// Настраиваем обработчики для новой строки
|
||||
const newRowElement = rowsContainer.lastElementChild;
|
||||
setupRowHandlers(newRowElement);
|
||||
});
|
||||
|
||||
// Настраиваем обработчики для всех строк
|
||||
rowsContainer.querySelectorAll('tr').forEach(row => {
|
||||
setupRowHandlers(row);
|
||||
});
|
||||
|
||||
// Валидация поля обоснования
|
||||
const reasonInput = document.getElementById('fillReason');
|
||||
reasonInput.addEventListener('input', function () {
|
||||
if (this.value.length >= 10) {
|
||||
this.classList.remove('is-invalid');
|
||||
this.classList.add('is-valid');
|
||||
} else if (this.value.length > 0) {
|
||||
this.classList.add('is-invalid');
|
||||
this.classList.remove('is-valid');
|
||||
} else {
|
||||
this.classList.remove('is-invalid', 'is-valid');
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик отправки формы
|
||||
document.getElementById('fillToolboxForm').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = document.getElementById('submitFillBtn');
|
||||
const spinner = document.getElementById('submitFillSpinner');
|
||||
const submitText = document.getElementById('submitFillText');
|
||||
|
||||
// Проверка заполнения всех обязательных полей
|
||||
const rows = rowsContainer.querySelectorAll('tr');
|
||||
const items = [];
|
||||
let isValid = true;
|
||||
let errorMessage = '';
|
||||
|
||||
// Проверка обоснования
|
||||
const reason = reasonInput.value.trim();
|
||||
if (reason.length < 10) {
|
||||
isValid = false;
|
||||
errorMessage = 'Введите обоснование (не менее 10 символов)';
|
||||
reasonInput.classList.add('is-invalid');
|
||||
reasonInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
} else {
|
||||
reasonInput.classList.remove('is-invalid');
|
||||
reasonInput.classList.add('is-valid');
|
||||
}
|
||||
|
||||
// Проверка строк с инструментами
|
||||
rows.forEach((row, index) => {
|
||||
const categorySelect = row.querySelector('.category-select');
|
||||
const toolkitSelect = row.querySelector('.toolkit-select');
|
||||
const quantityInput = row.querySelector('.quantity');
|
||||
const priceInput = row.querySelector('.price');
|
||||
|
||||
if (!categorySelect.value || !toolkitSelect.value ||
|
||||
!quantityInput.value || !priceInput.value) {
|
||||
isValid = false;
|
||||
errorMessage = `Заполните все обязательные поля в строке ${index + 1}`;
|
||||
// Подсвечиваем проблемные поля
|
||||
[categorySelect, toolkitSelect, quantityInput, priceInput].forEach(field => {
|
||||
if (!field.value) {
|
||||
field.classList.add('is-invalid');
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
items.push({
|
||||
toolkit_id: parseInt(toolkitSelect.value),
|
||||
quantity: parseInt(quantityInput.value),
|
||||
price: parseFloat(priceInput.value.replace(',', '.')),
|
||||
placement: row.querySelector('.placement').value || null
|
||||
});
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
document.getElementById('fillToolboxErrorMessage').textContent = errorMessage;
|
||||
document.getElementById('fillToolboxError').classList.remove('d-none');
|
||||
document.getElementById('fillToolboxError').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
document.getElementById('fillToolboxErrorMessage').textContent = 'Добавьте хотя бы одну позицию';
|
||||
document.getElementById('fillToolboxError').classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
// Запрос подтверждения (двойное нажатие в течение 10 секунд)
|
||||
const originalText = submitText.textContent;
|
||||
submitText.textContent = 'Нажмите еще раз для подтверждения (10 сек)';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
let confirmed = false;
|
||||
let timeout = setTimeout(() => {
|
||||
if (!confirmed) {
|
||||
submitText.textContent = originalText;
|
||||
submitBtn.disabled = false;
|
||||
document.getElementById('fillToolboxError').classList.add('d-none');
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
submitBtn.addEventListener('click', async function confirmHandler(e) {
|
||||
e.preventDefault();
|
||||
confirmed = true;
|
||||
clearTimeout(timeout);
|
||||
|
||||
// Показываем индикатор загрузки
|
||||
submitBtn.disabled = true;
|
||||
spinner.style.display = 'inline-block';
|
||||
|
||||
try {
|
||||
const response = await apiRequest('/toolbox/fill', {
|
||||
toolboxId: toolboxInfo.id,
|
||||
items: items,
|
||||
reason: reason,
|
||||
userId: userData.id
|
||||
}, 'POST');
|
||||
|
||||
if (response.status === 'ok') {
|
||||
showInfo('Склад успешно пополнен', 'success');
|
||||
bsModal.hide();
|
||||
|
||||
// Обновляем содержимое склада
|
||||
await loadToolboxContent(toolboxInfo.id);
|
||||
} else {
|
||||
throw new Error(response.message || 'Ошибка при пополнении склада');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при пополнении склада:', error);
|
||||
document.getElementById('fillToolboxErrorMessage').textContent =
|
||||
error.message || 'Произошла ошибка при пополнении склада';
|
||||
document.getElementById('fillToolboxError').classList.remove('d-none');
|
||||
|
||||
// Возвращаем кнопку в исходное состояние
|
||||
submitBtn.disabled = false;
|
||||
spinner.style.display = 'none';
|
||||
submitText.textContent = originalText;
|
||||
}
|
||||
|
||||
// Удаляем обработчик подтверждения
|
||||
submitBtn.removeEventListener('click', confirmHandler);
|
||||
}, { once: true });
|
||||
|
||||
submitBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Функция для настройки обработчиков строки
|
||||
function setupRowHandlers(row) {
|
||||
const rowId = row.id;
|
||||
|
||||
// Обработчик выбора категории
|
||||
const categorySelect = row.querySelector('.category-select');
|
||||
categorySelect.addEventListener('change', function () {
|
||||
this.classList.remove('is-invalid');
|
||||
const toolkitSelect = row.querySelector('.toolkit-select');
|
||||
const quantityInput = row.querySelector('.quantity');
|
||||
const priceInput = row.querySelector('.price');
|
||||
const placementInput = row.querySelector('.placement');
|
||||
|
||||
// Очищаем и деактивируем зависимые поля
|
||||
toolkitSelect.innerHTML = '<option value="">Выберите инструмент</option>';
|
||||
toolkitSelect.disabled = !this.value;
|
||||
quantityInput.disabled = !this.value;
|
||||
priceInput.disabled = !this.value;
|
||||
placementInput.disabled = !this.value;
|
||||
|
||||
if (!this.value) {
|
||||
quantityInput.value = '';
|
||||
priceInput.value = '';
|
||||
placementInput.value = '';
|
||||
row.querySelector('.cost').value = '0.00 ₽';
|
||||
updateTotals();
|
||||
return;
|
||||
}
|
||||
|
||||
// Заполняем список инструментов выбранной категории
|
||||
const categoryId = parseInt(this.value);
|
||||
const categoryTools = toolkitsByCategory[categoryId] || [];
|
||||
|
||||
categoryTools.forEach(tool => {
|
||||
const option = document.createElement('option');
|
||||
option.value = tool.id;
|
||||
option.textContent = tool.title;
|
||||
toolkitSelect.appendChild(option);
|
||||
});
|
||||
});
|
||||
|
||||
// Обработчик выбора инструмента
|
||||
const toolkitSelect = row.querySelector('.toolkit-select');
|
||||
toolkitSelect.addEventListener('change', function () {
|
||||
this.classList.remove('is-invalid');
|
||||
if (!this.value) return;
|
||||
|
||||
const toolId = parseInt(this.value);
|
||||
|
||||
// Автозаполнение расположения, если есть
|
||||
if (placementMap[toolId]) {
|
||||
row.querySelector('.placement').value = placementMap[toolId];
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчики изменения количества и цены
|
||||
const quantityInput = row.querySelector('.quantity');
|
||||
const priceInput = row.querySelector('.price');
|
||||
|
||||
[quantityInput, priceInput].forEach(input => {
|
||||
input.addEventListener('input', function () {
|
||||
this.classList.remove('is-invalid');
|
||||
calculateRowCost(row);
|
||||
});
|
||||
});
|
||||
|
||||
// Обработчик удаления строки
|
||||
const removeBtn = row.querySelector('.remove-row');
|
||||
removeBtn.addEventListener('click', function () {
|
||||
if (rowsContainer.children.length <= 1) return;
|
||||
|
||||
row.remove();
|
||||
updateTotals();
|
||||
|
||||
// Если осталась одна строка, делаем кнопку удаления недоступной
|
||||
if (rowsContainer.children.length === 1) {
|
||||
rowsContainer.querySelector('.remove-row').disabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Очистка при закрытии модального окна
|
||||
modal.addEventListener('hidden.bs.modal', () => {
|
||||
setTimeout(() => {
|
||||
if (modal.parentNode) {
|
||||
modal.remove();
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Показываем модальное окно
|
||||
bsModal.show();
|
||||
|
||||
// Устанавливаем фокус на первую категорию
|
||||
setTimeout(() => {
|
||||
const firstCategorySelect = rowsContainer.querySelector('.category-select');
|
||||
if (firstCategorySelect) {
|
||||
firstCategorySelect.focus();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function deleteToolbox(toolboxId) {
|
||||
|
||||
@@ -18,28 +18,28 @@ export function showInfo(message, type = 'info') {
|
||||
|
||||
// Определяем классы в зависимости от типа
|
||||
const typeConfig = {
|
||||
'success': { bgClass: 'bg-success', icon: 'bi-check-circle', delay: 5000 },
|
||||
'error': { bgClass: 'bg-danger', icon: 'bi-exclamation-circle', delay: 10000 },
|
||||
'info': { bgClass: 'bg-info', icon: 'bi-info-circle', delay: 3000 },
|
||||
'warning': { bgClass: 'bg-warning', icon: 'bi-exclamation-triangle', delay: 8000 }
|
||||
'success': { bgClass: 'bg-success', icon: 'bi-check-circle', delay: 5000, textColor: 'text-white' },
|
||||
'error': { bgClass: 'bg-danger', icon: 'bi-exclamation-circle', delay: 10000, textColor: 'text-white' },
|
||||
'info': { bgClass: 'bg-info', icon: 'bi-info-circle', delay: 3000, textColor: 'text-dark' },
|
||||
'warning': { bgClass: 'bg-warning', icon: 'bi-exclamation-triangle', delay: 8000, textColor: 'text-dark' }
|
||||
};
|
||||
|
||||
const config = typeConfig[type] || typeConfig.info;
|
||||
|
||||
// Создаем тост
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${config.bgClass} text-white`;
|
||||
toast.className = `toast ${config.bgClass} ${config.textColor} shadow-sm`;
|
||||
toast.id = toastId;
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.setAttribute('aria-live', 'assertive');
|
||||
toast.setAttribute('aria-atomic', 'true');
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="toast-header ${config.bgClass} text-white">
|
||||
<div class="toast-header ${config.bgClass} ${config.textColor}">
|
||||
<i class="bi ${config.icon} me-2"></i>
|
||||
<strong class="me-auto">Уведомление</strong>
|
||||
<small>Только что</small>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
|
||||
<button type="button" class="btn-close btn-close-dark" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
${message}
|
||||
|
||||
Reference in New Issue
Block a user