Files
PriceForge/web/templates/pricelists.html
2026-02-25 19:06:28 +03:00

332 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
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" .}}