Redesign configurator UI with tabs and remove Excel export
- Add tab-based configurator (Base, Storage, PCI, Power, Accessories, Other) - Base tab: single-select with autocomplete for MB, CPU, MEM - Other tabs: multi-select with autocomplete and quantity input - Table view with LOT, Description, Price, Quantity, Total columns - Add configuration list page with create modal (opportunity number) - Remove Excel export functionality and excelize dependency - Increase component list limit from 100 to 5000 - Add web templates (base, index, configs, login, admin_pricing) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
113
web/templates/base.html
Normal file
113
web/templates/base.html
Normal file
@@ -0,0 +1,113 @@
|
||||
{{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="/configs" class="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm" id="nav-configs" style="display:none;">Мои конфигурации</a>
|
||||
<a href="/admin/pricing" class="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm" id="nav-admin" style="display:none;">Цены</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div id="user-logged-out">
|
||||
<a href="/login" class="text-blue-600 hover:text-blue-800 text-sm">Войти</a>
|
||||
</div>
|
||||
<div id="user-logged-in" class="hidden">
|
||||
<span id="user-name" class="text-sm text-gray-700 mr-3"></span>
|
||||
<button onclick="logout()" class="text-red-600 hover:text-red-800 text-sm">Выйти</button>
|
||||
</div>
|
||||
</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 initAuth() {
|
||||
const token = localStorage.getItem('token');
|
||||
const user = localStorage.getItem('user');
|
||||
|
||||
if (token && user) {
|
||||
try {
|
||||
const userData = JSON.parse(user);
|
||||
document.getElementById('user-logged-out').classList.add('hidden');
|
||||
document.getElementById('user-logged-in').classList.remove('hidden');
|
||||
document.getElementById('user-name').textContent = userData.username;
|
||||
document.getElementById('nav-configs').style.display = 'block';
|
||||
if (userData.role === 'admin' || userData.role === 'pricing_admin') {
|
||||
document.getElementById('nav-admin').style.display = 'block';
|
||||
}
|
||||
} catch(e) {
|
||||
logout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
if (data.connected) {
|
||||
statusEl.innerHTML = '<span class="text-green-400">БД: подключено</span>';
|
||||
} 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>';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initAuth();
|
||||
checkDbStatus();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user