feat: добавлена интеграция синхронизации с Recommerce

This commit is contained in:
2026-01-12 21:45:31 +03:00
parent a5ab216934
commit 707b45b16d
13 changed files with 475 additions and 104 deletions

View File

@@ -0,0 +1,128 @@
document.addEventListener('DOMContentLoaded', function() {
const syncBtn = document.getElementById('bulk-recommerce-sync');
const modalEl = document.getElementById('recommerceSyncModal');
const startBtn = document.getElementById('startRecommerceSyncBtn');
if (!syncBtn || !modalEl) return;
const modal = new bootstrap.Modal(modalEl);
syncBtn.addEventListener('click', function(e) {
e.preventDefault();
// Получаем выбранные элементы
// Предполагается, что на странице есть механизм BatchSelection или просто чекбоксы
let selectedItems = [];
// Проверяем глобальный объект BatchSelection (из batch-selection.js)
if (window.BatchSelection && typeof window.BatchSelection.getSelectedItems === 'function') {
selectedItems = window.BatchSelection.getSelectedItems();
} else {
// Fallback: собираем вручную
document.querySelectorAll('.item-checkbox:checked').forEach(cb => {
selectedItems.push(cb.value);
});
}
// Фильтруем только товары (формат value="product:123")
// Комплекты (kit:123) пока игнорируем, так как бэкенд ожидает Product ID
const productIds = selectedItems
.filter(val => val.startsWith('product:'))
.map(val => val.split(':')[1]);
if (productIds.length === 0) {
// Если выбраны только комплекты или ничего не выбрано
if (selectedItems.length > 0) {
alert('Для синхронизации с Recommerce выберите товары (комплекты пока не поддерживаются).');
} else {
alert('Выберите товары для синхронизации.');
}
return;
}
// Обновляем UI модального окна
document.getElementById('recommerceSyncCount').textContent = productIds.length;
// Сохраняем ID для отправки
startBtn.dataset.productIds = JSON.stringify(productIds);
modal.show();
});
startBtn.addEventListener('click', function() {
const productIds = JSON.parse(this.dataset.productIds || '[]');
const options = {
fields: [],
create_if_missing: document.getElementById('syncCreateNew').checked
};
if (document.getElementById('syncPrice').checked) options.fields.push('price');
if (document.getElementById('syncStock').checked) options.fields.push('count');
if (document.getElementById('syncContent').checked) options.fields.push('content');
if (document.getElementById('syncImages').checked) options.fields.push('images');
// Блокируем кнопку
startBtn.disabled = true;
const originalText = startBtn.innerHTML;
startBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Запуск...';
fetch('/settings/integrations/recommerce/sync/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
product_ids: productIds,
options: options
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
modal.hide();
// Используем стандартный alert или toast, если есть
alert(`Синхронизация запущена успешно!\nID задачи: ${data.task_id}\nОбрабатывается товаров: ${productIds.length}`);
// Снимаем выделение
if (window.BatchSelection && typeof window.BatchSelection.clearSelection === 'function') {
window.BatchSelection.clearSelection();
}
} else {
alert('Ошибка при запуске: ' + (data.error || 'Неизвестная ошибка'));
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка сети или сервера');
})
.finally(() => {
startBtn.disabled = false;
startBtn.innerHTML = originalText;
});
});
// Helper для получения CSRF токена
function getCsrfToken() {
// Сначала пробуем получить из скрытого поля (для CSRF_USE_SESSIONS = True)
const csrfInput = document.querySelector('[name=csrfmiddlewaretoken]');
if (csrfInput) {
return csrfInput.value;
}
// Fallback: из куки
const name = 'csrftoken';
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
});

View File

@@ -0,0 +1,70 @@
<!-- Модальное окно для синхронизации с Recommerce -->
<div class="modal fade" id="recommerceSyncModal" tabindex="-1" aria-labelledby="recommerceSyncModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="recommerceSyncModalLabel">
<i class="bi bi-arrow-repeat"></i> Синхронизация с Recommerce
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<div class="alert alert-info mb-3">
<i class="bi bi-info-circle"></i> <strong>Выбрано товаров:</strong> <span id="recommerceSyncCount">0</span>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Что обновлять?</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="price" id="syncPrice" checked>
<label class="form-check-label" for="syncPrice">
Цены
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="count" id="syncStock" checked>
<label class="form-check-label" for="syncStock">
Остатки
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="content" id="syncContent">
<label class="form-check-label" for="syncContent">
Название и описание
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="images" id="syncImages">
<label class="form-check-label" for="syncImages">
Изображения
</label>
</div>
</div>
<hr>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="syncCreateNew">
<label class="form-check-label" for="syncCreateNew">
Создавать товары, если не найдены
</label>
<div class="form-text text-muted">
Если товар отсутствует в Recommerce, он будет создан (требуется полное заполнение).
</div>
</div>
</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="startRecommerceSyncBtn">
<i class="bi bi-play-fill"></i> Запустить
</button>
</div>
</div>
</div>
</div>

View File

@@ -4,6 +4,7 @@
{% block title %}Товары и комплекты{% endblock %}
{% block content %}
{% csrf_token %}
<div class="container-fluid mt-4">
<h2 class="mb-4">
<i class="bi bi-box-seam"></i> Товары и комплекты
@@ -188,6 +189,12 @@
<i class="bi bi-bookmark-fill"></i> Изменить категории
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="#" id="bulk-recommerce-sync">
<i class="bi bi-arrow-repeat"></i> Синхронизация с Recommerce
</a>
</li>
</ul>
</div>
</div>
@@ -398,6 +405,8 @@
{% endif %}
</div>
{% include "products/includes/recommerce_sync_modal.html" %}
<!-- Модальное окно для массового изменения категорий -->
<div class="modal fade" id="bulkCategoryModal" tabindex="-1" aria-labelledby="bulkCategoryModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
@@ -487,4 +496,5 @@
{% load static %}
<script src="{% static 'products/js/batch-selection.js' %}?v=1.2"></script>
<script src="{% static 'products/js/bulk-category-modal.js' %}?v=1.6"></script>
<script src="{% static 'products/js/recommerce-sync.js' %}?v=1.1"></script>
{% endblock %}