Add default showcase selection per warehouse
- Add is_default field to Showcase model with unique constraint per warehouse - Implement Showcase.save() to ensure only one default per warehouse - Add SetDefaultShowcaseView for AJAX-based default selection - Update ShowcaseForm to include is_default checkbox - Add interactive checkbox UI in showcase list with AJAX functionality - Update POS API to return showcase.is_default instead of warehouse.is_default - Update terminal.js to auto-select showcase based on its is_default flag - Add migration for is_default field 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -85,7 +85,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Is Active -->
|
||||
<div class="mb-4">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
{{ form.is_active }}
|
||||
<label class="form-check-label" for="{{ form.is_active.id_for_label }}">
|
||||
@@ -102,6 +102,24 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Is Default -->
|
||||
<div class="mb-4">
|
||||
<div class="form-check">
|
||||
{{ form.is_default }}
|
||||
<label class="form-check-label" for="{{ form.is_default.id_for_label }}">
|
||||
{{ form.is_default.label }}
|
||||
</label>
|
||||
{% if form.is_default.help_text %}
|
||||
<div><small class="form-text text-muted">{{ form.is_default.help_text }}</small></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if form.is_default.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.is_default.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<!-- Скрытое поле для CSRF токена (нужно для AJAX запросов) -->
|
||||
<div style="display: none;">
|
||||
{% csrf_token %}
|
||||
</div>
|
||||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
@@ -77,6 +81,7 @@
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 40px;">✓</th>
|
||||
<th>Название</th>
|
||||
<th>Склад</th>
|
||||
<th>Описание</th>
|
||||
@@ -90,15 +95,26 @@
|
||||
{% for warehouse_group in showcases_by_warehouse %}
|
||||
<!-- Warehouse Header Row -->
|
||||
<tr class="table-secondary">
|
||||
<td colspan="6" class="fw-bold">
|
||||
<td colspan="7" class="fw-bold">
|
||||
<i class="bi bi-building me-2"></i>{{ warehouse_group.grouper.name }}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Showcases in this warehouse -->
|
||||
{% for showcase in warehouse_group.list %}
|
||||
<tr>
|
||||
<tr {% if showcase.is_default %}class="table-warning"{% endif %} data-showcase-id="{{ showcase.pk }}" data-warehouse-id="{{ showcase.warehouse.id }}">
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="default-showcase-checkbox"
|
||||
data-showcase-id="{{ showcase.pk }}"
|
||||
data-warehouse-id="{{ showcase.warehouse.id }}"
|
||||
data-set-default-url="{% url 'inventory:showcase-set-default' showcase.pk %}"
|
||||
{% if showcase.is_default %}checked{% endif %}
|
||||
style="cursor: pointer; width: 18px; height: 18px;">
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ showcase.name }}</strong>
|
||||
{% if showcase.is_default %}
|
||||
<span class="badge bg-warning text-dark ms-2">По умолчанию</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-muted">{{ showcase.warehouse.name }}</span>
|
||||
@@ -159,4 +175,132 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Обработчик для галочек "По умолчанию"
|
||||
const checkboxes = document.querySelectorAll('.default-showcase-checkbox');
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
const showcaseId = this.dataset.showcaseId;
|
||||
const warehouseId = this.dataset.warehouseId;
|
||||
const setDefaultUrl = this.dataset.setDefaultUrl;
|
||||
|
||||
// Получаем CSRF токен
|
||||
let csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value;
|
||||
|
||||
if (!csrfToken) {
|
||||
csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
}
|
||||
|
||||
if (!csrfToken) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
csrfToken = cookieValue;
|
||||
}
|
||||
|
||||
// Если галочка установлена, отправляем запрос
|
||||
if (this.checked) {
|
||||
// Визуально обновляем таблицу сразу (оптимистичное обновление)
|
||||
// Снимаем флаги только с витрин того же склада
|
||||
document.querySelectorAll(`tr[data-warehouse-id="${warehouseId}"]`).forEach(tr => {
|
||||
const cb = tr.querySelector('.default-showcase-checkbox');
|
||||
if (cb) {
|
||||
cb.checked = false;
|
||||
}
|
||||
tr.classList.remove('table-warning');
|
||||
tr.querySelector('.badge.bg-warning')?.remove();
|
||||
});
|
||||
|
||||
// Отмечаем текущую строку
|
||||
this.checked = true;
|
||||
const currentRow = document.querySelector(`tr[data-showcase-id="${showcaseId}"]`);
|
||||
currentRow.classList.add('table-warning');
|
||||
|
||||
// Добавляем бейдж "По умолчанию" если его нет
|
||||
const nameCell = currentRow.querySelector('td:nth-child(2)');
|
||||
if (!nameCell.querySelector('.badge.bg-warning')) {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'badge bg-warning text-dark ms-2';
|
||||
badge.textContent = 'По умолчанию';
|
||||
nameCell.appendChild(badge);
|
||||
}
|
||||
|
||||
// Отправляем AJAX запрос
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
if (csrfToken) {
|
||||
headers['X-CSRFToken'] = csrfToken;
|
||||
}
|
||||
|
||||
fetch(setDefaultUrl, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error(`HTTP ${response.status}: ${text}`);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
showNotification(data.message, 'success');
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Ошибка при запросе:', error);
|
||||
showNotification('Ошибка при установке витрины по умолчанию: ' + error.message, 'error');
|
||||
// Перезагружаем через 2 секунды
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Функция для показа уведомлений
|
||||
function showNotification(message, type = 'info') {
|
||||
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
const alertHtml = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const container = document.querySelector('.container-fluid');
|
||||
const alertElement = document.createElement('div');
|
||||
alertElement.innerHTML = alertHtml;
|
||||
container.insertBefore(alertElement.firstElementChild, container.firstChild);
|
||||
|
||||
// Автоматически скрываем через 4 секунды
|
||||
setTimeout(() => {
|
||||
const alert = container.querySelector('.alert');
|
||||
if (alert) {
|
||||
alert.remove();
|
||||
}
|
||||
}, 4000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user