Files
QuoteForge/web/templates/partnumber_books.html
Michael Chus 5e56f386cc feat: implement vendor spec BOM import and PN→LOT resolution (Phase 1)
- Migration 029: local_partnumber_books, local_partnumber_book_items,
  vendor_spec TEXT column on local_configurations
- Models: LocalPartnumberBook, LocalPartnumberBookItem, VendorSpec,
  VendorSpecItem with JSON Valuer/Scanner
- Repository: PartnumberBookRepository (GetActiveBook, FindLotByPartnumber,
  SaveBook/Items, ListBooks, CountBookItems)
- Service: VendorSpecResolver 3-step resolution (book → manual suggestion
  → unresolved) + AggregateLOTs with is_primary_pn qty logic
- Sync: PullPartnumberBooks append-only pull from qt_partnumber_books
- Handlers: VendorSpecHandler (GET/PUT/resolve/apply), PartnumberBooksHandler
- Routes: /api/configs/:uuid/vendor-spec*, /api/partnumber-books,
  /api/sync/partnumber-books, /partnumber-books page
- UI: 3 top-level tabs [Estimate][BOM вендора][Ценообразование]; Excel paste,
  PN resolution, inline LOT autocomplete, pricing table
- Bible: 03-database.md updated, 09-vendor-spec.md added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 10:22:22 +03:00

140 lines
5.7 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"}}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>
<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>
<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>
</tr>
</thead>
<tbody id="books-table-body"></tbody>
</table>
<div id="books-empty" class="hidden p-8 text-center text-gray-400">
Нет листов сопоставлений. Синхронизируйте с сервером.
</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>
</div>
{{end}}
{{define "scripts"}}
<script>
async function loadBooks() {
const resp = await fetch('/api/partnumber-books');
const data = await resp.json();
const books = data.books || [];
document.getElementById('books-list-loading').classList.add('hidden');
if (!books.length) {
document.getElementById('books-empty').classList.remove('hidden');
return;
}
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>
`;
tbody.appendChild(tr);
});
document.getElementById('books-table').classList.remove('hidden');
}
async function viewBookItems(serverId, version) {
const resp = await fetch(`/api/partnumber-books/${serverId}`);
const data = await resp.json();
const items = data.items || [];
document.getElementById('detail-version').textContent = version;
const tbody = document.getElementById('detail-items-body');
tbody.innerHTML = '';
items.forEach(item => {
const tr = document.createElement('tr');
tr.className = 'border-b';
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>
`;
tbody.appendChild(tr);
});
document.getElementById('book-detail').classList.remove('hidden');
document.getElementById('book-detail').scrollIntoView({behavior: 'smooth'});
}
function closeBookDetail() {
document.getElementById('book-detail').classList.add('hidden');
}
async function syncPartnumberBooks() {
try {
const resp = await fetch('/api/sync/partnumber-books', {method: 'POST'});
const data = await resp.json();
if (data.success) {
showToast(`Синхронизировано: ${data.synced} листов`, 'success');
loadBooks();
} else {
showToast('Ошибка: ' + data.error, 'error');
}
} catch (e) {
showToast('Ошибка синхронизации', 'error');
}
}
document.addEventListener('DOMContentLoaded', loadBooks);
</script>
{{end}}
{{template "base" .}}