- 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>
140 lines
5.7 KiB
HTML
140 lines
5.7 KiB
HTML
{{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" .}}
|