- Pass originalHTML through syncAction function chain - Simplify finally block by restoring original button innerHTML - Remove hardcoded button HTML values (5 lines reduction) - Improve maintainability: button text changes won't break code - Preserve any custom classes, attributes, or nested elements This fixes the issue where originalHTML was declared but never used. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
220 lines
9.7 KiB
HTML
220 lines
9.7 KiB
HTML
{{define "base"}}
|
|
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{template "title" .}}</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
|
<style>
|
|
.htmx-request { opacity: 0.5; }
|
|
.line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-100 min-h-screen">
|
|
<nav class="bg-white shadow-sm sticky top-0 z-40">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between h-14">
|
|
<div class="flex items-center space-x-8">
|
|
<a href="/" class="text-xl font-bold text-blue-600">QuoteForge</a>
|
|
<div class="hidden md:flex space-x-4">
|
|
<a href="/pricelists" class="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm">Прайслисты</a>
|
|
<a href="/configurator" class="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm">Конфигуратор</a>
|
|
<a id="admin-pricing-link" href="/admin/pricing" class="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm hidden">Администратор цен</a>
|
|
<a href="/setup" class="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm">Настройки</a>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center space-x-4">
|
|
<!-- Sync Status Indicator (htmx-powered) -->
|
|
<div id="sync-status"
|
|
class="flex items-center gap-3 text-sm"
|
|
hx-get="/partials/sync-status"
|
|
hx-trigger="load, refresh from:body, every 30s"
|
|
hx-swap="innerHTML">
|
|
</div>
|
|
<span id="db-user" class="text-sm text-gray-600"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 pb-12">
|
|
{{template "content" .}}
|
|
</main>
|
|
|
|
<div id="toast" class="fixed bottom-4 right-4 z-50"></div>
|
|
|
|
<footer class="fixed bottom-0 left-0 right-0 bg-gray-800 text-gray-300 text-xs py-1 px-4">
|
|
<div class="max-w-7xl mx-auto flex justify-between">
|
|
<span id="db-status">БД: проверка...</span>
|
|
<span id="db-counts"></span>
|
|
</div>
|
|
</footer>
|
|
|
|
<script>
|
|
function showToast(msg, type) {
|
|
const colors = { success: 'bg-green-500', error: 'bg-red-500', info: 'bg-blue-500' };
|
|
const el = document.getElementById('toast');
|
|
el.innerHTML = '<div class="' + (colors[type] || colors.info) + ' text-white px-4 py-2 rounded shadow">' + msg + '</div>';
|
|
setTimeout(() => el.innerHTML = '', 3000);
|
|
}
|
|
|
|
// Event delegation for sync dropdown and actions
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
checkDbStatus();
|
|
checkWritePermission();
|
|
});
|
|
|
|
// Handle keyboard navigation for dropdown
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
const dropdownMenu = document.getElementById('sync-dropdown-menu');
|
|
if (dropdownMenu) {
|
|
dropdownMenu.classList.add('hidden');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Event delegation for all sync actions
|
|
document.body.addEventListener('click', function(e) {
|
|
// Handle dropdown toggle
|
|
const dropdownButton = e.target.closest('#sync-dropdown-button');
|
|
if (dropdownButton) {
|
|
e.stopPropagation();
|
|
const dropdownMenu = document.getElementById('sync-dropdown-menu');
|
|
if (dropdownMenu) {
|
|
dropdownMenu.classList.toggle('hidden');
|
|
// Update aria-expanded
|
|
const isExpanded = dropdownMenu.classList.contains('hidden');
|
|
dropdownButton.setAttribute('aria-expanded', !isExpanded);
|
|
}
|
|
}
|
|
|
|
// Handle sync actions
|
|
const actionButton = e.target.closest('[data-action]');
|
|
if (actionButton) {
|
|
const action = actionButton.dataset.action;
|
|
const button = actionButton; // Keep reference to original button
|
|
|
|
// Add loading state
|
|
const originalHTML = button.innerHTML;
|
|
button.disabled = true;
|
|
button.innerHTML = '<svg class="animate-spin w-4 h-4 inline mr-2" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Синхронизация...';
|
|
|
|
if (action === 'push-changes') {
|
|
pushPendingChanges(button, originalHTML);
|
|
} else if (action === 'full-sync') {
|
|
fullSync(button, originalHTML);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Close dropdown when clicking outside
|
|
document.body.addEventListener('click', function(e) {
|
|
const dropdownButton = document.getElementById('sync-dropdown-button');
|
|
const dropdownMenu = document.getElementById('sync-dropdown-menu');
|
|
|
|
if (dropdownButton && dropdownMenu &&
|
|
!dropdownButton.contains(e.target) &&
|
|
!dropdownMenu.contains(e.target)) {
|
|
dropdownMenu.classList.add('hidden');
|
|
if (dropdownButton) {
|
|
dropdownButton.setAttribute('aria-expanded', 'false');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Refactored sync action function to reduce duplication
|
|
async function syncAction(endpoint, successMessage, button, originalHTML) {
|
|
try {
|
|
const resp = await fetch(endpoint, { method: 'POST' });
|
|
const data = await resp.json();
|
|
|
|
if (data.success) {
|
|
showToast(successMessage, 'success');
|
|
// Update last sync time
|
|
loadLastSyncTime();
|
|
} else {
|
|
showToast('Ошибка: ' + (data.error || 'неизвестная ошибка'), 'error');
|
|
}
|
|
|
|
htmx.trigger('#sync-status', 'refresh');
|
|
} catch (error) {
|
|
showToast('Ошибка: ' + error.message, 'error');
|
|
} finally {
|
|
// Reset button state
|
|
if (button) {
|
|
button.disabled = false;
|
|
button.innerHTML = originalHTML;
|
|
}
|
|
}
|
|
}
|
|
|
|
function pushPendingChanges(button, originalHTML) {
|
|
syncAction('/api/sync/push', 'Изменения синхронизированы', button, originalHTML);
|
|
}
|
|
|
|
function fullSync(button, originalHTML) {
|
|
syncAction('/api/sync/all', 'Полная синхронизация завершена', button, originalHTML);
|
|
}
|
|
|
|
async function checkDbStatus() {
|
|
try {
|
|
const resp = await fetch('/api/db-status');
|
|
const data = await resp.json();
|
|
const statusEl = document.getElementById('db-status');
|
|
const countsEl = document.getElementById('db-counts');
|
|
const userEl = document.getElementById('db-user');
|
|
|
|
if (data.connected) {
|
|
statusEl.innerHTML = '<span class="text-green-400">БД: подключено</span>';
|
|
if (data.db_user) {
|
|
userEl.innerHTML = '<span class="text-gray-500">@</span>' + data.db_user;
|
|
}
|
|
} else {
|
|
statusEl.innerHTML = '<span class="text-red-400">БД: ошибка - ' + data.error + '</span>';
|
|
}
|
|
|
|
countsEl.textContent = 'lot: ' + data.lot_count + ' | lot_log: ' + data.lot_log_count + ' | metadata: ' + data.metadata_count;
|
|
} catch(e) {
|
|
document.getElementById('db-status').innerHTML = '<span class="text-red-400">БД: нет связи</span>';
|
|
}
|
|
}
|
|
|
|
async function checkWritePermission() {
|
|
try {
|
|
const resp = await fetch('/api/pricelists/can-write');
|
|
const data = await resp.json();
|
|
if (data.can_write) {
|
|
const link = document.getElementById('admin-pricing-link');
|
|
if (link) link.classList.remove('hidden');
|
|
}
|
|
} catch(e) {
|
|
console.error('Failed to check write permission:', e);
|
|
}
|
|
}
|
|
|
|
// Load last sync time for dropdown
|
|
async function loadLastSyncTime() {
|
|
try {
|
|
const resp = await fetch('/api/sync/status');
|
|
const data = await resp.json();
|
|
if (data.last_pricelist_sync) {
|
|
const date = new Date(data.last_pricelist_sync);
|
|
document.getElementById('last-sync-time').textContent = date.toLocaleString('ru-RU');
|
|
} else {
|
|
document.getElementById('last-sync-time').textContent = 'Нет данных';
|
|
}
|
|
} catch(e) {
|
|
console.error('Failed to load last sync time:', e);
|
|
}
|
|
}
|
|
|
|
// Load last sync time when page loads
|
|
document.addEventListener('DOMContentLoaded', loadLastSyncTime);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|