332 lines
15 KiB
HTML
332 lines
15 KiB
HTML
{{define "title"}}Прайслисты - PriceForge{{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-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="7" 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-orange-600 text-white rounded-md hover:bg-orange-700">
|
||
Создать
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Price Changes Modal -->
|
||
<div id="price-changes-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-4xl w-full mx-4 max-h-[85vh] overflow-hidden flex flex-col">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h2 class="text-xl font-bold">Изменения цен</h2>
|
||
<button type="button" onclick="closePriceChangesModal()" class="text-gray-500 hover:text-gray-700">Закрыть</button>
|
||
</div>
|
||
<div id="price-changes-summary" class="text-sm text-gray-600 mb-4"></div>
|
||
<div id="price-changes-content" class="overflow-auto space-y-4"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let canWrite = false;
|
||
let currentPage = 1;
|
||
|
||
function escapeHtml(value) {
|
||
return String(value ?? '')
|
||
.replaceAll('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll("'", ''');
|
||
}
|
||
|
||
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-orange-600 text-white rounded-md hover:bg-orange-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="7" class="px-6 py-4 text-center text-red-500">
|
||
Ошибка загрузки: ${escapeHtml(e.message)}
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
}
|
||
|
||
function renderPricelists(pricelists) {
|
||
if (pricelists.length === 0) {
|
||
document.getElementById('pricelists-body').innerHTML = `
|
||
<tr>
|
||
<td colspan="7" 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 statusClass = pl.is_active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
|
||
const statusText = pl.is_active ? 'Активен' : 'Неактивен';
|
||
const id = Number(pl.id);
|
||
const safeID = Number.isFinite(id) ? id : 0;
|
||
const safeVersion = escapeHtml(pl.version || '');
|
||
const safeDate = escapeHtml(date);
|
||
const safeCreatedBy = escapeHtml(pl.created_by || '-');
|
||
const safeItemCount = Number.isFinite(Number(pl.item_count)) ? Number(pl.item_count) : 0;
|
||
const safeUsageCount = Number.isFinite(Number(pl.usage_count)) ? Number(pl.usage_count) : 0;
|
||
|
||
let actions = `<a href="/pricelists/${safeID}" class="text-orange-600 hover:text-orange-800 text-sm">Просмотр</a>`;
|
||
if (canWrite && safeUsageCount === 0 && safeID > 0) {
|
||
actions += ` <button onclick="deletePricelist(${safeID})" 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">${safeVersion}</span>
|
||
</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${safeDate}</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${safeCreatedBy}</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm">${safeItemCount}</td>
|
||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm">${safeUsageCount}</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-orange-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');
|
||
}
|
||
|
||
function closePriceChangesModal() {
|
||
document.getElementById('price-changes-modal').classList.add('hidden');
|
||
document.getElementById('price-changes-modal').classList.remove('flex');
|
||
}
|
||
|
||
function formatPrice(value) {
|
||
if (value === null || value === undefined || Number.isNaN(Number(value))) return '-';
|
||
return Number(value).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
}
|
||
|
||
function renderPriceChangesTable(items, { showNewPrice = true, showDiff = true } = {}) {
|
||
if (!items || items.length === 0) return '<div class="text-sm text-gray-500">Нет данных</div>';
|
||
const rows = items.map(item => {
|
||
const changeType = item.change_type || '';
|
||
const rowClass = changeType === 'increased'
|
||
? 'bg-red-50'
|
||
: (changeType === 'decreased' ? 'bg-green-50' : '');
|
||
const diffValue = Number(item.diff || 0);
|
||
const diffText = showDiff && item.diff !== null && item.diff !== undefined
|
||
? `${diffValue > 0 ? '+' : ''}${formatPrice(diffValue)}`
|
||
: '-';
|
||
const diffClass = diffValue > 0 ? 'text-red-700' : (diffValue < 0 ? 'text-green-700' : 'text-gray-700');
|
||
return `
|
||
<tr class="${rowClass}">
|
||
<td class="px-3 py-2 font-mono text-xs">${escapeHtml(item.lot_name || '')}</td>
|
||
<td class="px-3 py-2 text-right">${formatPrice(item.old_price)}</td>
|
||
${showNewPrice ? `<td class="px-3 py-2 text-right">${item.new_price == null ? '-' : formatPrice(item.new_price)}</td>` : ''}
|
||
${showDiff ? `<td class="px-3 py-2 text-right ${diffClass}">${diffText}</td>` : ''}
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
|
||
return `
|
||
<div class="border rounded-lg overflow-hidden">
|
||
<table class="min-w-full text-sm">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th class="px-3 py-2 text-left">Позиция</th>
|
||
<th class="px-3 py-2 text-right">Было</th>
|
||
${showNewPrice ? '<th class="px-3 py-2 text-right">Стало</th>' : ''}
|
||
${showDiff ? '<th class="px-3 py-2 text-right">Изм.</th>' : ''}
|
||
</tr>
|
||
</thead>
|
||
<tbody>${rows}</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function openPriceChangesModal(pl) {
|
||
const changes = pl?.price_changes || {};
|
||
const changed = Array.isArray(changes.changed) ? changes.changed : [];
|
||
const missing = Array.isArray(changes.missing) ? changes.missing : [];
|
||
const prevVersion = changes.previous_pricelist_version || null;
|
||
|
||
document.getElementById('price-changes-summary').innerHTML = prevVersion
|
||
? `Сравнение с прайслистом <span class="font-mono">${escapeHtml(prevVersion)}</span>. Изменилось: <b>${changed.length}</b>, пропало (нет котировок): <b>${missing.length}</b>.`
|
||
: 'Предыдущий прайслист для сравнения не найден.';
|
||
|
||
let html = '';
|
||
html += `<div><h3 class="font-semibold mb-2">Изменившиеся цены</h3>${renderPriceChangesTable(changed)}</div>`;
|
||
html += `<div><h3 class="font-semibold mb-2">Пропали котировки (цена была раньше)</h3>${renderPriceChangesTable(missing, { showNewPrice: true, showDiff: false })}</div>`;
|
||
document.getElementById('price-changes-content').innerHTML = html;
|
||
|
||
document.getElementById('price-changes-modal').classList.remove('hidden');
|
||
document.getElementById('price-changes-modal').classList.add('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');
|
||
openPriceChangesModal(pl);
|
||
loadPricelists(1);
|
||
} catch (e) {
|
||
showToast('Ошибка: ' + e.message, 'error');
|
||
}
|
||
});
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
checkPricelistWritePermission();
|
||
loadPricelists(1);
|
||
});
|
||
</script>
|
||
{{end}}
|
||
|
||
{{template "base" .}}
|