- Refactored navbar sync button to dispatch 'sync-completed' event - Configs page: removed duplicate 'Импорт с сервера' button, added auto-refresh on sync - Projects page: wrapped initialization in DOMContentLoaded, added auto-refresh on sync - Pricelists page: added auto-refresh on sync completion - Consistent UX: all lists update automatically after 'Синхронизация' button click - Removed code duplication: importConfigsFromServer() function no longer needed - Event-driven architecture enables easy extension to other pages Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
249 lines
10 KiB
HTML
249 lines
10 KiB
HTML
{{define "title"}}Прайслисты - QuoteForge{{end}}
|
|
|
|
{{define "content"}}
|
|
<div class="space-y-6">
|
|
<div class="flex justify-between items-center">
|
|
<h1 class="text-2xl font-bold text-gray-900">Прайслисты</h1>
|
|
<div id="create-btn-container"></div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-lg shadow overflow-hidden">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Версия</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Тип</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Дата</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Автор</th>
|
|
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase">Позиций</th>
|
|
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase">Исп.</th>
|
|
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase">Статус</th>
|
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Действия</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="pricelists-body" class="bg-white divide-y divide-gray-200">
|
|
<tr>
|
|
<td colspan="8" class="px-6 py-4 text-center text-gray-500">Загрузка...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div id="pagination" class="flex justify-center space-x-2"></div>
|
|
</div>
|
|
|
|
<!-- Create Modal -->
|
|
<div id="create-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
|
<div class="bg-white rounded-lg p-6 max-w-md w-full mx-4">
|
|
<h2 class="text-xl font-bold mb-4">Создать прайслист</h2>
|
|
<p class="text-sm text-gray-600 mb-4">
|
|
Будет создан снимок текущих цен из базы данных.<br>
|
|
Автор прайслиста: <span id="db-username" class="font-medium">загрузка...</span>
|
|
</p>
|
|
<form id="create-form" class="space-y-4">
|
|
<div class="flex justify-end space-x-3">
|
|
<button type="button" onclick="closeCreateModal()"
|
|
class="px-4 py-2 text-gray-700 border border-gray-300 rounded-md hover:bg-gray-50">
|
|
Отмена
|
|
</button>
|
|
<button type="submit"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
|
|
Создать
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let canWrite = false;
|
|
let currentPage = 1;
|
|
|
|
async function checkPricelistWritePermission() {
|
|
try {
|
|
const resp = await fetch('/api/pricelists/can-write');
|
|
const data = await resp.json();
|
|
canWrite = data.can_write;
|
|
|
|
if (canWrite) {
|
|
document.getElementById('create-btn-container').innerHTML = `
|
|
<button onclick="openCreateModal()" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
|
|
Создать прайслист
|
|
</button>
|
|
`;
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to check pricelist write permission:', e);
|
|
}
|
|
}
|
|
|
|
async function loadPricelists(page = 1) {
|
|
currentPage = page;
|
|
try {
|
|
const resp = await fetch(`/api/pricelists?page=${page}&per_page=20`);
|
|
const data = await resp.json();
|
|
|
|
renderPricelists(data.pricelists || []);
|
|
renderPagination(data.total, data.page, data.per_page);
|
|
} catch (e) {
|
|
document.getElementById('pricelists-body').innerHTML = `
|
|
<tr>
|
|
<td colspan="8" class="px-6 py-4 text-center text-red-500">
|
|
Ошибка загрузки: ${e.message}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function renderPricelists(pricelists) {
|
|
if (pricelists.length === 0) {
|
|
document.getElementById('pricelists-body').innerHTML = `
|
|
<tr>
|
|
<td colspan="8" class="px-6 py-4 text-center text-gray-500">
|
|
Прайслисты не найдены. ${canWrite ? 'Создайте первый прайслист.' : ''}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const html = pricelists.map(pl => {
|
|
const date = new Date(pl.created_at).toLocaleDateString('ru-RU');
|
|
const sourceToType = {
|
|
estimate: 'estimate',
|
|
warehouse: 'stock',
|
|
competitor: 'b2b'
|
|
};
|
|
const pricelistType = sourceToType[pl.source] || pl.source || '-';
|
|
const statusClass = pl.is_active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
|
|
const statusText = pl.is_active ? 'Активен' : 'Неактивен';
|
|
|
|
let actions = `<a href="/pricelists/${pl.id}" class="text-blue-600 hover:text-blue-800 text-sm">Просмотр</a>`;
|
|
if (canWrite && pl.usage_count === 0) {
|
|
actions += ` <button onclick="deletePricelist(${pl.id})" class="text-red-600 hover:text-red-800 text-sm ml-2">Удалить</button>`;
|
|
}
|
|
|
|
return `
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span class="font-mono text-sm">${pl.version}</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${pricelistType}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${date}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${pl.created_by || '-'}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-center text-sm">${pl.item_count}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-center text-sm">${pl.usage_count}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-center">
|
|
<span class="px-2 py-1 text-xs rounded-full ${statusClass}">${statusText}</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right">${actions}</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
|
|
document.getElementById('pricelists-body').innerHTML = html;
|
|
}
|
|
|
|
function renderPagination(total, page, perPage) {
|
|
const totalPages = Math.ceil(total / perPage);
|
|
if (totalPages <= 1) {
|
|
document.getElementById('pagination').innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
for (let i = 1; i <= totalPages; i++) {
|
|
const activeClass = i === page ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-gray-50';
|
|
html += `<button onclick="loadPricelists(${i})" class="px-3 py-1 rounded border ${activeClass}">${i}</button>`;
|
|
}
|
|
|
|
document.getElementById('pagination').innerHTML = html;
|
|
}
|
|
|
|
async function loadDbUsername() {
|
|
try {
|
|
const resp = await fetch('/api/current-user');
|
|
const data = await resp.json();
|
|
document.getElementById('db-username').textContent = data.username || 'неизвестно';
|
|
} catch (e) {
|
|
document.getElementById('db-username').textContent = 'неизвестно';
|
|
}
|
|
}
|
|
|
|
function openCreateModal() {
|
|
document.getElementById('create-modal').classList.remove('hidden');
|
|
document.getElementById('create-modal').classList.add('flex');
|
|
loadDbUsername();
|
|
}
|
|
|
|
function closeCreateModal() {
|
|
document.getElementById('create-modal').classList.add('hidden');
|
|
document.getElementById('create-modal').classList.remove('flex');
|
|
}
|
|
|
|
async function createPricelist() {
|
|
const resp = await fetch('/api/pricelists', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({})
|
|
});
|
|
|
|
if (!resp.ok) {
|
|
const data = await resp.json();
|
|
throw new Error(data.error || 'Failed to create pricelist');
|
|
}
|
|
|
|
return await resp.json();
|
|
}
|
|
|
|
async function deletePricelist(id) {
|
|
if (!confirm('Удалить этот прайслист?')) return;
|
|
|
|
try {
|
|
const resp = await fetch(`/api/pricelists/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (!resp.ok) {
|
|
const data = await resp.json();
|
|
throw new Error(data.error || 'Failed to delete');
|
|
}
|
|
|
|
showToast('Прайслист удален', 'success');
|
|
loadPricelists(currentPage);
|
|
} catch (e) {
|
|
showToast('Ошибка: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
document.getElementById('create-form').addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
try {
|
|
const pl = await createPricelist();
|
|
closeCreateModal();
|
|
showToast(`Прайслист ${pl.version} создан (${pl.item_count} позиций)`, 'success');
|
|
loadPricelists(1);
|
|
} catch (e) {
|
|
showToast('Ошибка: ' + e.message, 'error');
|
|
}
|
|
});
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
checkPricelistWritePermission();
|
|
loadPricelists(1);
|
|
|
|
// Listen for sync completion events from navbar
|
|
window.addEventListener('sync-completed', function(e) {
|
|
// Reload pricelists on sync completion
|
|
loadPricelists(1);
|
|
});
|
|
});
|
|
</script>
|
|
{{end}}
|
|
|
|
{{template "base" .}}
|