Add vendor workspace import and pricing export workflow
This commit is contained in:
@@ -29,30 +29,6 @@
|
||||
Нет активного листа сопоставлений. Нажмите «Синхронизировать» для загрузки с сервера.
|
||||
</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) -->
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<!-- Header row — always visible -->
|
||||
@@ -92,14 +68,51 @@
|
||||
</div>
|
||||
</div>
|
||||
</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="onItemsSearchInput(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 id="items-pagination" class="hidden px-4 py-3 border-t flex items-center justify-between text-sm text-gray-600">
|
||||
<span id="items-page-info"></span>
|
||||
<div class="flex gap-2">
|
||||
<button id="items-prev" onclick="changeItemsPage(-1)" class="px-3 py-1 rounded border hover:bg-gray-50 disabled:opacity-40 disabled:cursor-not-allowed">← Назад</button>
|
||||
<button id="items-next" onclick="changeItemsPage(1)" class="px-3 py-1 rounded border hover:bg-gray-50 disabled:opacity-40 disabled:cursor-not-allowed">Вперёд →</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
let allItems = [];
|
||||
let allBooks = [];
|
||||
let booksPage = 1;
|
||||
const BOOKS_PER_PAGE = 10;
|
||||
const ITEMS_PER_PAGE = 100;
|
||||
let activeBookServerID = null;
|
||||
let activeItems = [];
|
||||
let itemsPage = 1;
|
||||
let itemsTotal = 0;
|
||||
let itemsSearch = '';
|
||||
let _itemsSearchTimer = null;
|
||||
|
||||
function toggleBooksSection() {
|
||||
const body = document.getElementById('books-section-body');
|
||||
@@ -179,29 +192,46 @@ function changeBooksPage(delta) {
|
||||
}
|
||||
|
||||
async function loadActiveBookItems(book) {
|
||||
activeBookServerID = book.server_id;
|
||||
return loadActiveBookItemsPage(1, itemsSearch, book);
|
||||
}
|
||||
|
||||
async function loadActiveBookItemsPage(page = 1, search = '', book = null) {
|
||||
const targetBook = book || allBooks.find(b => b.server_id === activeBookServerID);
|
||||
if (!targetBook) return;
|
||||
|
||||
let resp, data;
|
||||
try {
|
||||
resp = await fetch(`/api/partnumber-books/${book.server_id}`);
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
per_page: String(ITEMS_PER_PAGE),
|
||||
});
|
||||
if (search.trim()) {
|
||||
params.set('search', search.trim());
|
||||
}
|
||||
resp = await fetch(`/api/partnumber-books/${targetBook.server_id}?` + params.toString());
|
||||
data = await resp.json();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
if (!resp.ok) return;
|
||||
|
||||
allItems = data.items || [];
|
||||
activeItems = data.items || [];
|
||||
itemsPage = data.page || page;
|
||||
itemsTotal = Number(data.total || 0);
|
||||
itemsSearch = data.search || search || '';
|
||||
|
||||
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('card-version').textContent = targetBook.version;
|
||||
document.getElementById('card-date').textContent = targetBook.created_at;
|
||||
document.getElementById('card-lots').textContent = Number(data.lot_count || 0);
|
||||
document.getElementById('card-pn-total').textContent = Number(data.book_total || 0);
|
||||
document.getElementById('card-pn-primary').textContent = Number(data.primary_count || 0);
|
||||
document.getElementById('summary-cards').classList.remove('hidden');
|
||||
document.getElementById('active-book-section').classList.remove('hidden');
|
||||
document.getElementById('pn-search').value = itemsSearch;
|
||||
|
||||
renderItems(allItems);
|
||||
renderItems(activeItems);
|
||||
renderItemsPagination();
|
||||
}
|
||||
|
||||
function renderItems(items) {
|
||||
@@ -218,15 +248,38 @@ function renderItems(items) {
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
document.getElementById('pn-count').textContent = `${items.length} из ${allItems.length}`;
|
||||
const totalLabel = itemsSearch ? `${items.length} из ${itemsTotal} по фильтру` : `${items.length} из ${itemsTotal}`;
|
||||
document.getElementById('pn-count').textContent = totalLabel;
|
||||
}
|
||||
|
||||
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)
|
||||
));
|
||||
function renderItemsPagination() {
|
||||
const totalPages = Math.max(1, Math.ceil(itemsTotal / ITEMS_PER_PAGE));
|
||||
const start = itemsTotal === 0 ? 0 : ((itemsPage - 1) * ITEMS_PER_PAGE) + 1;
|
||||
const end = Math.min(itemsPage * ITEMS_PER_PAGE, itemsTotal);
|
||||
const box = document.getElementById('items-pagination');
|
||||
if (itemsTotal > ITEMS_PER_PAGE) {
|
||||
box.classList.remove('hidden');
|
||||
document.getElementById('items-page-info').textContent = `Сопоставления ${start}–${end} из ${itemsTotal}`;
|
||||
document.getElementById('items-prev').disabled = itemsPage === 1;
|
||||
document.getElementById('items-next').disabled = itemsPage >= totalPages;
|
||||
} else {
|
||||
box.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function changeItemsPage(delta) {
|
||||
const totalPages = Math.max(1, Math.ceil(itemsTotal / ITEMS_PER_PAGE));
|
||||
const nextPage = Math.max(1, Math.min(totalPages, itemsPage + delta));
|
||||
if (nextPage === itemsPage) return;
|
||||
loadActiveBookItemsPage(nextPage, itemsSearch);
|
||||
}
|
||||
|
||||
function onItemsSearchInput(value) {
|
||||
clearTimeout(_itemsSearchTimer);
|
||||
_itemsSearchTimer = setTimeout(() => {
|
||||
itemsSearch = value.trim();
|
||||
loadActiveBookItemsPage(1, itemsSearch);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
async function syncPartnumberBooks() {
|
||||
@@ -253,4 +306,3 @@ document.addEventListener('DOMContentLoaded', loadBooks);
|
||||
{{end}}
|
||||
|
||||
{{template "base" .}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user