feat: add Партномера nav item and summary page
- Top nav: link to /partnumber-books - Page: summary cards (active version, unique LOTs, total PN, primary PN) + searchable items table for active book + collapsible history of all snapshots Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
<div class="hidden md:flex space-x-4">
|
||||
<a href="/projects" class="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm">Мои проекты</a>
|
||||
<a href="/pricelists" class="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm">Прайслисты</a>
|
||||
<a href="/partnumber-books" class="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm">Партномера</a>
|
||||
<a href="/setup" class="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm">Настройки</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,60 +1,91 @@
|
||||
{{define "title"}}QuoteForge - Листы сопоставлений{{end}}
|
||||
{{define "title"}}QuoteForge - Партномера{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Листы сопоставлений PN → LOT</h1>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Партномера</h1>
|
||||
<button onclick="syncPartnumberBooks()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm">
|
||||
Синхронизировать
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div id="books-list-loading" class="p-8 text-center text-gray-400">Загрузка...</div>
|
||||
<!-- Summary cards -->
|
||||
<div id="summary-cards" class="grid grid-cols-2 md:grid-cols-4 gap-4 hidden">
|
||||
<div class="bg-white rounded-lg shadow p-4">
|
||||
<div class="text-xs text-gray-500 mb-1">Активный лист</div>
|
||||
<div id="card-version" class="font-mono font-semibold text-gray-800 text-sm truncate">—</div>
|
||||
<div id="card-date" class="text-xs text-gray-400 mt-0.5">—</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-4">
|
||||
<div class="text-xs text-gray-500 mb-1">Уникальных LOT</div>
|
||||
<div id="card-lots" class="text-2xl font-bold text-blue-600">—</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-4">
|
||||
<div class="text-xs text-gray-500 mb-1">Всего PN</div>
|
||||
<div id="card-pn-total" class="text-2xl font-bold text-gray-800">—</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-4">
|
||||
<div class="text-xs text-gray-500 mb-1">Primary PN</div>
|
||||
<div id="card-pn-primary" class="text-2xl font-bold text-green-600">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="summary-empty" class="hidden bg-yellow-50 border border-yellow-200 rounded-lg p-4 text-sm text-yellow-800">
|
||||
Нет активного листа сопоставлений. Нажмите «Синхронизировать» для загрузки с сервера.
|
||||
</div>
|
||||
|
||||
<!-- Active book items with search -->
|
||||
<div id="active-book-section" class="hidden bg-white rounded-lg shadow overflow-hidden">
|
||||
<div class="px-4 py-3 border-b flex items-center justify-between gap-3">
|
||||
<span class="font-semibold text-gray-800 whitespace-nowrap">Сопоставления активного листа</span>
|
||||
<input type="text" id="pn-search" placeholder="Поиск по PN или LOT..."
|
||||
class="flex-1 max-w-xs px-3 py-1.5 border rounded text-sm focus:ring-2 focus:ring-blue-400 focus:outline-none"
|
||||
oninput="filterItems(this.value)">
|
||||
<span id="pn-count" class="text-xs text-gray-400 whitespace-nowrap"></span>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-gray-600 sticky top-0">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left">Partnumber</th>
|
||||
<th class="px-4 py-2 text-left">LOT</th>
|
||||
<th class="px-4 py-2 text-center w-24">Primary</th>
|
||||
<th class="px-4 py-2 text-left">Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="active-items-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- All books list (collapsed by default) -->
|
||||
<details class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<summary class="px-4 py-3 cursor-pointer text-sm font-medium text-gray-700 hover:bg-gray-50 select-none">
|
||||
История снимков
|
||||
</summary>
|
||||
<div id="books-list-loading" class="p-6 text-center text-gray-400 text-sm">Загрузка...</div>
|
||||
<table id="books-table" class="w-full text-sm hidden">
|
||||
<thead class="bg-gray-50 text-gray-600">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left">Версия</th>
|
||||
<th class="px-4 py-3 text-left">Дата</th>
|
||||
<th class="px-4 py-3 text-right">Позиций</th>
|
||||
<th class="px-4 py-3 text-center">Статус</th>
|
||||
<th class="px-4 py-3 text-center">Действия</th>
|
||||
<th class="px-4 py-2 text-left">Версия</th>
|
||||
<th class="px-4 py-2 text-left">Дата</th>
|
||||
<th class="px-4 py-2 text-right">Позиций</th>
|
||||
<th class="px-4 py-2 text-center">Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="books-table-body"></tbody>
|
||||
</table>
|
||||
<div id="books-empty" class="hidden p-8 text-center text-gray-400">
|
||||
Нет листов сопоставлений. Синхронизируйте с сервером.
|
||||
<div id="books-empty" class="hidden p-6 text-center text-gray-400 text-sm">
|
||||
Нет загруженных снимков.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Book detail -->
|
||||
<div id="book-detail" class="hidden bg-white rounded-lg shadow overflow-hidden">
|
||||
<div class="px-4 py-3 border-b flex items-center justify-between">
|
||||
<h2 class="font-semibold text-gray-800">Позиции листа: <span id="detail-version"></span></h2>
|
||||
<button onclick="closeBookDetail()" class="text-gray-400 hover:text-gray-700">✕</button>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 text-gray-600">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">Partnumber</th>
|
||||
<th class="px-3 py-2 text-left">LOT</th>
|
||||
<th class="px-3 py-2 text-center">Primary PN</th>
|
||||
<th class="px-3 py-2 text-left">Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="detail-items-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "scripts"}}
|
||||
<script>
|
||||
let allItems = [];
|
||||
|
||||
async function loadBooks() {
|
||||
const resp = await fetch('/api/partnumber-books');
|
||||
const data = await resp.json();
|
||||
@@ -64,57 +95,78 @@ async function loadBooks() {
|
||||
|
||||
if (!books.length) {
|
||||
document.getElementById('books-empty').classList.remove('hidden');
|
||||
document.getElementById('summary-empty').classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill history table
|
||||
const tbody = document.getElementById('books-table-body');
|
||||
tbody.innerHTML = '';
|
||||
books.forEach(b => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'border-b hover:bg-gray-50';
|
||||
tr.innerHTML = `
|
||||
<td class="px-4 py-3 font-mono text-sm">${b.version}</td>
|
||||
<td class="px-4 py-3 text-gray-600">${b.created_at}</td>
|
||||
<td class="px-4 py-3 text-right">${b.item_count}</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
${b.is_active ? '<span class="px-2 py-1 bg-green-100 text-green-700 rounded text-xs">Активный</span>' : '<span class="px-2 py-1 bg-gray-100 text-gray-500 rounded text-xs">Архив</span>'}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<button onclick="viewBookItems(${b.server_id}, '${b.version}')" class="text-blue-600 hover:text-blue-800 text-sm underline">
|
||||
Просмотр
|
||||
</button>
|
||||
<td class="px-4 py-2 font-mono text-xs">${b.version}</td>
|
||||
<td class="px-4 py-2 text-gray-500 text-xs">${b.created_at}</td>
|
||||
<td class="px-4 py-2 text-right text-xs">${b.item_count}</td>
|
||||
<td class="px-4 py-2 text-center">
|
||||
${b.is_active
|
||||
? '<span class="px-2 py-0.5 bg-green-100 text-green-700 rounded text-xs">Активный</span>'
|
||||
: '<span class="px-2 py-0.5 bg-gray-100 text-gray-500 rounded text-xs">Архив</span>'}
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
document.getElementById('books-table').classList.remove('hidden');
|
||||
|
||||
// Load active book detail
|
||||
const active = books.find(b => b.is_active) || books[0];
|
||||
await loadActiveBookItems(active);
|
||||
}
|
||||
|
||||
async function viewBookItems(serverId, version) {
|
||||
const resp = await fetch(`/api/partnumber-books/${serverId}`);
|
||||
async function loadActiveBookItems(book) {
|
||||
const resp = await fetch(`/api/partnumber-books/${book.server_id}`);
|
||||
const data = await resp.json();
|
||||
const items = data.items || [];
|
||||
allItems = data.items || [];
|
||||
|
||||
document.getElementById('detail-version').textContent = version;
|
||||
const tbody = document.getElementById('detail-items-body');
|
||||
// Compute stats
|
||||
const lots = new Set(allItems.map(i => i.lot_name));
|
||||
const primaryCount = allItems.filter(i => i.is_primary_pn).length;
|
||||
|
||||
document.getElementById('card-version').textContent = book.version;
|
||||
document.getElementById('card-date').textContent = book.created_at;
|
||||
document.getElementById('card-lots').textContent = lots.size;
|
||||
document.getElementById('card-pn-total').textContent = allItems.length;
|
||||
document.getElementById('card-pn-primary').textContent = primaryCount;
|
||||
document.getElementById('summary-cards').classList.remove('hidden');
|
||||
document.getElementById('active-book-section').classList.remove('hidden');
|
||||
|
||||
renderItems(allItems);
|
||||
}
|
||||
|
||||
function renderItems(items) {
|
||||
const tbody = document.getElementById('active-items-body');
|
||||
tbody.innerHTML = '';
|
||||
items.forEach(item => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'border-b';
|
||||
tr.className = 'border-b hover:bg-gray-50';
|
||||
tr.innerHTML = `
|
||||
<td class="px-3 py-1.5 font-mono text-xs">${item.partnumber}</td>
|
||||
<td class="px-3 py-1.5 text-xs">${item.lot_name}</td>
|
||||
<td class="px-3 py-1.5 text-center">${item.is_primary_pn ? '✓' : ''}</td>
|
||||
<td class="px-3 py-1.5 text-xs text-gray-500">${item.description || ''}</td>
|
||||
<td class="px-4 py-1.5 font-mono text-xs">${item.partnumber}</td>
|
||||
<td class="px-4 py-1.5 text-xs font-medium text-blue-700">${item.lot_name}</td>
|
||||
<td class="px-4 py-1.5 text-center text-green-600 text-xs">${item.is_primary_pn ? '✓' : ''}</td>
|
||||
<td class="px-4 py-1.5 text-xs text-gray-500">${item.description || ''}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
document.getElementById('book-detail').classList.remove('hidden');
|
||||
document.getElementById('book-detail').scrollIntoView({behavior: 'smooth'});
|
||||
document.getElementById('pn-count').textContent = `${items.length} из ${allItems.length}`;
|
||||
}
|
||||
|
||||
function closeBookDetail() {
|
||||
document.getElementById('book-detail').classList.add('hidden');
|
||||
function filterItems(query) {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) { renderItems(allItems); return; }
|
||||
renderItems(allItems.filter(i =>
|
||||
i.partnumber.toLowerCase().includes(q) || i.lot_name.toLowerCase().includes(q)
|
||||
));
|
||||
}
|
||||
|
||||
async function syncPartnumberBooks() {
|
||||
|
||||
Reference in New Issue
Block a user