Add stock pricelist admin flow with mapping placeholders and warehouse details
This commit is contained in:
@@ -8,8 +8,9 @@
|
||||
<div class="flex justify-between items-center border-b pb-4 mb-4">
|
||||
<div class="flex gap-4">
|
||||
<button onclick="loadTab('alerts')" id="btn-alerts" class="text-blue-600 font-medium">Алерты</button>
|
||||
<button onclick="loadTab('components')" id="btn-components" class="text-gray-600">Компоненты</button>
|
||||
<button onclick="loadTab('pricelists')" id="btn-pricelists" class="text-gray-600">Прайслисты</button>
|
||||
<button onclick="loadTab('estimate')" id="btn-estimate" class="text-gray-600">Estimate</button>
|
||||
<button onclick="loadTab('warehouse')" id="btn-warehouse" class="text-gray-600">Склад</button>
|
||||
<button onclick="loadTab('competitor')" id="btn-competitor" class="text-gray-600">Конкуренты</button>
|
||||
<button onclick="loadTab('sync-status')" id="btn-sync-status" class="text-gray-600 hidden">Статус синхронизации</button>
|
||||
<button onclick="loadTab('all-configs')" id="btn-all-configs" class="text-gray-600 hidden">Все конфигурации</button>
|
||||
</div>
|
||||
@@ -58,8 +59,15 @@
|
||||
<!-- Pricelists Tab Content (hidden by default) -->
|
||||
<div id="pricelists-tab-content" class="hidden">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-semibold">Прайслисты</h2>
|
||||
<div id="pricelists-create-btn-container"></div>
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 id="pricelists-title" class="text-xl font-semibold">Estimate</h2>
|
||||
<button id="estimate-settings-btn" onclick="loadTab('component-settings')" class="hidden px-3 py-2 border border-gray-300 rounded-md text-sm hover:bg-gray-50">
|
||||
Настройка компонентов
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div id="pricelists-create-btn-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
@@ -84,6 +92,52 @@
|
||||
</div>
|
||||
|
||||
<div id="pricelists-pagination" class="flex justify-center space-x-2 mt-4"></div>
|
||||
|
||||
<div id="stock-tools" class="mt-6 hidden space-y-6">
|
||||
<div class="border rounded-lg p-4">
|
||||
<h3 class="text-lg font-semibold mb-3">Импорт stock_log</h3>
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="file" id="stock-file-input" accept=".mxl,.xlsx" class="block w-full text-sm text-gray-700">
|
||||
<button onclick="importStockFile()" class="px-4 py-2 bg-emerald-600 text-white rounded hover:bg-emerald-700">Импортировать</button>
|
||||
</div>
|
||||
<div id="stock-import-progress" class="hidden mt-4 p-3 bg-blue-50 border border-blue-200 rounded">
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span id="stock-import-status">Запуск...</span>
|
||||
<span id="stock-import-percent">0%</span>
|
||||
</div>
|
||||
<div class="w-full bg-blue-100 rounded-full h-3">
|
||||
<div id="stock-import-bar" class="h-3 bg-blue-600 rounded-full transition-all duration-300" style="width:0%"></div>
|
||||
</div>
|
||||
<div id="stock-import-stats" class="text-xs text-gray-600 mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border rounded-lg p-4">
|
||||
<h3 class="text-lg font-semibold mb-3">Сопоставление partnumber -> LOT</h3>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<input id="mapping-partnumber" type="text" placeholder="partnumber" class="px-3 py-2 border rounded w-1/3">
|
||||
<input id="mapping-lotname" type="text" placeholder="lot_name" class="px-3 py-2 border rounded w-1/3">
|
||||
<button onclick="saveStockMapping()" class="px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Сохранить</button>
|
||||
<button onclick="loadStockMappings(1)" class="px-3 py-2 border rounded hover:bg-gray-50">Обновить</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Partnumber</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Описание</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">LOT</th>
|
||||
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="stock-mappings-body" class="bg-white divide-y divide-gray-200">
|
||||
<tr><td colspan="4" class="px-4 py-3 text-sm text-gray-500">Загрузка...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="stock-mappings-pagination" class="flex justify-center space-x-2 mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync Status Tab Content (hidden by default) -->
|
||||
@@ -239,6 +293,7 @@
|
||||
|
||||
<script>
|
||||
let currentTab = 'alerts';
|
||||
let currentPricelistSource = 'estimate';
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
let perPage = 50;
|
||||
@@ -252,6 +307,7 @@ let pricelistsCanWrite = false;
|
||||
let isCreatingPricelist = false;
|
||||
let cachedDbUsername = null;
|
||||
let syncUsersStatusTimer = null;
|
||||
let stockMappingsPage = 1;
|
||||
|
||||
async function loadTab(tab) {
|
||||
currentTab = tab;
|
||||
@@ -263,36 +319,36 @@ async function loadTab(tab) {
|
||||
}
|
||||
|
||||
document.getElementById('btn-alerts').className = tab === 'alerts' ? 'text-blue-600 font-medium' : 'text-gray-600';
|
||||
document.getElementById('btn-components').className = tab === 'components' ? 'text-blue-600 font-medium' : 'text-gray-600';
|
||||
document.getElementById('btn-pricelists').className = tab === 'pricelists' ? 'text-blue-600 font-medium' : 'text-gray-600';
|
||||
document.getElementById('btn-estimate').className = (tab === 'estimate' || tab === 'component-settings') ? 'text-blue-600 font-medium' : 'text-gray-600';
|
||||
document.getElementById('btn-warehouse').className = tab === 'warehouse' ? 'text-blue-600 font-medium' : 'text-gray-600';
|
||||
document.getElementById('btn-competitor').className = tab === 'competitor' ? 'text-blue-600 font-medium' : 'text-gray-600';
|
||||
document.getElementById('btn-sync-status').className = (tab === 'sync-status' ? 'text-blue-600 font-medium' : 'text-gray-600') + (pricelistsCanWrite ? '' : ' hidden');
|
||||
document.getElementById('btn-all-configs').className = tab === 'all-configs' ? 'text-blue-600 font-medium' : 'text-gray-600 hidden';
|
||||
|
||||
// Show/hide elements based on tab
|
||||
if (tab === 'components') {
|
||||
document.getElementById('search-bar').className = 'mb-4';
|
||||
document.getElementById('pagination').className = 'flex justify-between items-center mt-4 pt-4 border-t';
|
||||
document.getElementById('btn-all-configs').className = 'text-gray-600 hidden'; // Hide this tab for components
|
||||
document.getElementById('pricelists-tab-content').className = 'hidden';
|
||||
document.getElementById('sync-status-tab-content').className = 'hidden';
|
||||
document.getElementById('tab-content').className = '';
|
||||
} else if (tab === 'all-configs') {
|
||||
document.getElementById('search-bar').className = 'mb-4 hidden'; // Hide search for all configs
|
||||
document.getElementById('pagination').className = 'flex justify-between items-center mt-4 pt-4 border-t'; // Show pagination
|
||||
document.getElementById('btn-all-configs').className = 'text-blue-600 font-medium'; // Show this tab for all configs
|
||||
document.getElementById('pricelists-tab-content').className = 'hidden';
|
||||
document.getElementById('sync-status-tab-content').className = 'hidden';
|
||||
document.getElementById('tab-content').className = '';
|
||||
} else if (tab === 'pricelists') {
|
||||
if (tab === 'estimate' || tab === 'warehouse' || tab === 'competitor') {
|
||||
currentPricelistSource = tab === 'estimate' ? 'estimate' : (tab === 'warehouse' ? 'warehouse' : 'competitor');
|
||||
document.getElementById('search-bar').className = 'mb-4 hidden';
|
||||
document.getElementById('pagination').className = 'hidden';
|
||||
document.getElementById('btn-all-configs').className = 'text-gray-600 hidden';
|
||||
document.getElementById('pricelists-tab-content').className = '';
|
||||
document.getElementById('sync-status-tab-content').className = 'hidden';
|
||||
document.getElementById('tab-content').className = 'hidden';
|
||||
// Load pricelists when pricelists tab is selected
|
||||
checkPricelistWritePermission();
|
||||
loadPricelists(1);
|
||||
document.getElementById('pricelists-title').textContent = tab === 'estimate' ? 'Estimate' : (tab === 'warehouse' ? 'Склад' : 'Конкуренты');
|
||||
document.getElementById('estimate-settings-btn').classList.toggle('hidden', tab !== 'estimate');
|
||||
document.getElementById('stock-tools').classList.toggle('hidden', tab !== 'warehouse');
|
||||
await checkPricelistWritePermission();
|
||||
await loadPricelists(1, currentPricelistSource);
|
||||
if (tab === 'warehouse') {
|
||||
await loadStockMappings(1);
|
||||
}
|
||||
} else if (tab === 'component-settings') {
|
||||
document.getElementById('search-bar').className = 'mb-4';
|
||||
document.getElementById('pagination').className = 'flex justify-between items-center mt-4 pt-4 border-t';
|
||||
document.getElementById('btn-all-configs').className = 'text-gray-600 hidden';
|
||||
document.getElementById('pricelists-tab-content').className = 'hidden';
|
||||
document.getElementById('sync-status-tab-content').className = 'hidden';
|
||||
document.getElementById('tab-content').className = '';
|
||||
await loadData();
|
||||
} else if (tab === 'sync-status') {
|
||||
document.getElementById('search-bar').className = 'mb-4 hidden';
|
||||
document.getElementById('pagination').className = 'hidden';
|
||||
@@ -307,6 +363,14 @@ async function loadTab(tab) {
|
||||
}
|
||||
await loadUsersSyncStatus();
|
||||
startSyncUsersStatusRefresh();
|
||||
} else if (tab === 'all-configs') {
|
||||
document.getElementById('search-bar').className = 'mb-4 hidden';
|
||||
document.getElementById('pagination').className = 'flex justify-between items-center mt-4 pt-4 border-t';
|
||||
document.getElementById('btn-all-configs').className = 'text-blue-600 font-medium';
|
||||
document.getElementById('pricelists-tab-content').className = 'hidden';
|
||||
document.getElementById('sync-status-tab-content').className = 'hidden';
|
||||
document.getElementById('tab-content').className = '';
|
||||
await loadData();
|
||||
} else {
|
||||
document.getElementById('search-bar').className = 'mb-4 hidden';
|
||||
document.getElementById('pagination').className = 'hidden';
|
||||
@@ -314,9 +378,6 @@ async function loadTab(tab) {
|
||||
document.getElementById('pricelists-tab-content').className = 'hidden';
|
||||
document.getElementById('sync-status-tab-content').className = 'hidden';
|
||||
document.getElementById('tab-content').className = '';
|
||||
}
|
||||
|
||||
if (tab !== 'pricelists' && tab !== 'sync-status') {
|
||||
await loadData();
|
||||
}
|
||||
}
|
||||
@@ -356,7 +417,7 @@ async function loadData() {
|
||||
totalPages = Math.ceil(data.total / perPage);
|
||||
renderAllConfigs(data.configurations || []);
|
||||
updatePagination(data.total);
|
||||
} else {
|
||||
} else if (currentTab === 'component-settings') {
|
||||
let url = '/api/admin/pricing/components?page=' + currentPage + '&per_page=' + perPage;
|
||||
if (currentSearch) {
|
||||
url += '&search=' + encodeURIComponent(currentSearch);
|
||||
@@ -373,6 +434,8 @@ async function loadData() {
|
||||
componentsCache = data.components || [];
|
||||
renderComponents(componentsCache, data.total);
|
||||
updatePagination(data.total);
|
||||
} else {
|
||||
document.getElementById('tab-content').innerHTML = '<div class="text-center py-8 text-gray-500">Нет данных для вкладки</div>';
|
||||
}
|
||||
} catch(e) {
|
||||
document.getElementById('tab-content').innerHTML = '<div class="text-center py-8 text-red-600">Ошибка загрузки</div>';
|
||||
@@ -848,7 +911,7 @@ function recalculateAll() {
|
||||
progressBar.className = 'bg-green-600 h-4 rounded-full';
|
||||
setTimeout(() => {
|
||||
progressContainer.style.display = 'none';
|
||||
if (currentTab === 'components') {
|
||||
if (currentTab === 'component-settings') {
|
||||
loadData();
|
||||
}
|
||||
}, 2000);
|
||||
@@ -966,11 +1029,164 @@ function renderAllConfigs(configs) {
|
||||
document.getElementById('tab-content').innerHTML = html;
|
||||
}
|
||||
|
||||
async function importStockFile() {
|
||||
const input = document.getElementById('stock-file-input');
|
||||
if (!input.files || input.files.length === 0) {
|
||||
showToast('Выберите файл .mxl или .xlsx', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const file = input.files[0];
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
|
||||
const box = document.getElementById('stock-import-progress');
|
||||
const statusEl = document.getElementById('stock-import-status');
|
||||
const percentEl = document.getElementById('stock-import-percent');
|
||||
const barEl = document.getElementById('stock-import-bar');
|
||||
const statsEl = document.getElementById('stock-import-stats');
|
||||
box.classList.remove('hidden');
|
||||
statusEl.textContent = 'Запуск импорта...';
|
||||
percentEl.textContent = '0%';
|
||||
barEl.style.width = '0%';
|
||||
statsEl.textContent = '';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/admin/pricing/stock/import', {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
});
|
||||
if (!resp.ok) {
|
||||
let data = {};
|
||||
try { data = await resp.json(); } catch (_) {}
|
||||
throw new Error(data.error || 'Ошибка запуска импорта');
|
||||
}
|
||||
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const text = decoder.decode(value);
|
||||
const lines = text.split('\n');
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data:')) continue;
|
||||
let data;
|
||||
try { data = JSON.parse(line.slice(5).trim()); } catch (_) { continue; }
|
||||
const current = Number(data.current || 0);
|
||||
const total = Number(data.total || 100);
|
||||
const pct = total > 0 ? Math.round((current / total) * 100) : 0;
|
||||
barEl.style.width = pct + '%';
|
||||
percentEl.textContent = pct + '%';
|
||||
statusEl.textContent = data.message || data.status || 'Обработка';
|
||||
statsEl.textContent =
|
||||
`Валидных: ${data.valid_rows || 0} | Вставлено: ${data.inserted || 0} | ` +
|
||||
`Пропущено: ${(data.unmapped || 0) + (data.conflicts || 0) + (data.parse_errors || 0)} | ` +
|
||||
`Конфликты: ${data.conflicts || 0}`;
|
||||
|
||||
if (data.status === 'error') {
|
||||
throw new Error(data.message || 'Ошибка импорта');
|
||||
}
|
||||
if (data.status === 'completed') {
|
||||
showToast('Импорт stock_log завершен', 'success');
|
||||
await loadPricelists(1, 'warehouse');
|
||||
await loadStockMappings(stockMappingsPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Ошибка импорта: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStockMappings(page = 1) {
|
||||
stockMappingsPage = page;
|
||||
const body = document.getElementById('stock-mappings-body');
|
||||
const pagination = document.getElementById('stock-mappings-pagination');
|
||||
if (!body || !pagination) return;
|
||||
try {
|
||||
const resp = await fetch(`/api/admin/pricing/stock/mappings?page=${page}&per_page=20`);
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data.error || 'Ошибка загрузки');
|
||||
const items = data.items || [];
|
||||
if (items.length === 0) {
|
||||
body.innerHTML = '<tr><td colspan="4" class="px-4 py-3 text-sm text-gray-500">Нет сопоставлений</td></tr>';
|
||||
} else {
|
||||
body.innerHTML = items.map(item => `
|
||||
<tr>
|
||||
<td class="px-4 py-2 text-sm font-mono">${escapeHtml(item.partnumber)}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-600">${escapeHtml(item.description || '—')}</td>
|
||||
<td class="px-4 py-2 text-sm font-mono">${escapeHtml(item.lot_name || '—')}</td>
|
||||
<td class="px-4 py-2 text-right">
|
||||
<button data-partnumber="${escapeHtml(item.partnumber)}" onclick="deleteStockMapping(this.dataset.partnumber)" class="text-red-600 hover:text-red-800 text-sm">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil((data.total || 0) / (data.per_page || 20));
|
||||
if (totalPages <= 1) {
|
||||
pagination.innerHTML = '';
|
||||
} else {
|
||||
let html = '';
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
const cls = i === page ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-gray-50';
|
||||
html += `<button onclick="loadStockMappings(${i})" class="px-3 py-1 rounded border ${cls}">${i}</button>`;
|
||||
}
|
||||
pagination.innerHTML = html;
|
||||
}
|
||||
} catch (e) {
|
||||
body.innerHTML = `<tr><td colspan="4" class="px-4 py-3 text-sm text-red-600">${escapeHtml(e.message)}</td></tr>`;
|
||||
pagination.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveStockMapping() {
|
||||
const partnumber = document.getElementById('mapping-partnumber').value.trim();
|
||||
const lotName = document.getElementById('mapping-lotname').value.trim();
|
||||
if (!partnumber || !lotName) {
|
||||
showToast('Заполните partnumber и lot_name', 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch('/api/admin/pricing/stock/mappings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ partnumber: partnumber, lot_name: lotName })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data.error || 'Ошибка сохранения');
|
||||
document.getElementById('mapping-partnumber').value = '';
|
||||
document.getElementById('mapping-lotname').value = '';
|
||||
showToast('Сопоставление сохранено', 'success');
|
||||
await loadStockMappings(1);
|
||||
} catch (e) {
|
||||
showToast('Ошибка: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteStockMapping(partnumber) {
|
||||
if (!confirm('Удалить сопоставление для ' + partnumber + '?')) return;
|
||||
try {
|
||||
const resp = await fetch(`/api/admin/pricing/stock/mappings/${encodeURIComponent(partnumber)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data.error || 'Ошибка удаления');
|
||||
showToast('Сопоставление удалено', 'success');
|
||||
await loadStockMappings(stockMappingsPage);
|
||||
} catch (e) {
|
||||
showToast('Ошибка: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await checkPricelistWritePermission();
|
||||
// Check URL params for initial tab
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const initialTab = urlParams.get('tab') || 'alerts';
|
||||
let initialTab = urlParams.get('tab') || 'alerts';
|
||||
if (initialTab === 'pricelists') initialTab = 'estimate';
|
||||
if (initialTab === 'components') initialTab = 'component-settings';
|
||||
await loadTab(initialTab);
|
||||
|
||||
// Add event listeners for preview updates
|
||||
@@ -1081,10 +1297,10 @@ async function loadUsersSyncStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPricelists(page = 1) {
|
||||
async function loadPricelists(page = 1, source = currentPricelistSource) {
|
||||
pricelistsPage = page;
|
||||
try {
|
||||
const resp = await fetch(`/api/pricelists?page=${page}&per_page=20`);
|
||||
const resp = await fetch(`/api/pricelists?page=${page}&per_page=20&source=${encodeURIComponent(source)}`);
|
||||
const data = await resp.json();
|
||||
|
||||
renderPricelists(data.pricelists || []);
|
||||
@@ -1169,7 +1385,7 @@ async function togglePricelistActive(id, isActive) {
|
||||
}
|
||||
|
||||
showToast('Статус прайслиста обновлен', 'success');
|
||||
loadPricelists(pricelistsPage);
|
||||
loadPricelists(pricelistsPage, currentPricelistSource);
|
||||
} catch (e) {
|
||||
showToast('Ошибка: ' + e.message, 'error');
|
||||
}
|
||||
@@ -1185,7 +1401,7 @@ function renderPricelistsPagination(total, page, perPage) {
|
||||
let html = '';
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
const activeClass = i === page ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-gray-50';
|
||||
html += `<button onclick="loadPricelists(${i})" class="px-3 py-1 rounded border ${activeClass}">${i}</button>`;
|
||||
html += `<button onclick="loadPricelists(${i}, '${currentPricelistSource}')" class="px-3 py-1 rounded border ${activeClass}">${i}</button>`;
|
||||
}
|
||||
|
||||
document.getElementById('pricelists-pagination').innerHTML = html;
|
||||
@@ -1250,6 +1466,9 @@ async function createPricelist() {
|
||||
|
||||
const resp = await fetch('/api/pricelists/create-with-progress', {
|
||||
method: 'POST'
|
||||
,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ source: currentPricelistSource })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
@@ -1341,7 +1560,7 @@ async function deletePricelist(id) {
|
||||
}
|
||||
|
||||
showToast('Прайслист удален', 'success');
|
||||
loadPricelists(pricelistsPage);
|
||||
loadPricelists(pricelistsPage, currentPricelistSource);
|
||||
} catch (e) {
|
||||
showToast('Ошибка: ' + e.message, 'error');
|
||||
}
|
||||
@@ -1361,7 +1580,7 @@ document.getElementById('pricelists-create-form').addEventListener('submit', asy
|
||||
const pl = await createPricelist();
|
||||
closePricelistsCreateModal();
|
||||
showToast(`Прайслист ${pl.version} создан (${pl.item_count} позиций)`, 'success');
|
||||
loadPricelists(1);
|
||||
loadPricelists(1, currentPricelistSource);
|
||||
} catch (e) {
|
||||
showToast('Ошибка: ' + e.message, 'error');
|
||||
} finally {
|
||||
|
||||
@@ -57,13 +57,15 @@
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Артикул</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Категория</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Описание</th>
|
||||
<th id="th-qty" class="hidden px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Доступно</th>
|
||||
<th id="th-partnumbers" class="hidden px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Partnumbers</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Цена, $</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Настройки</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="items-body" class="bg-white divide-y divide-gray-200">
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-4 text-center text-gray-500">Загрузка...</td>
|
||||
<td colspan="7" class="px-6 py-4 text-center text-gray-500">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -80,6 +82,7 @@
|
||||
let currentPage = 1;
|
||||
let searchQuery = '';
|
||||
let searchTimeout = null;
|
||||
let currentSource = '';
|
||||
|
||||
async function loadPricelistInfo() {
|
||||
try {
|
||||
@@ -87,6 +90,8 @@
|
||||
if (!resp.ok) throw new Error('Pricelist not found');
|
||||
|
||||
const pl = await resp.json();
|
||||
currentSource = pl.source || '';
|
||||
toggleWarehouseColumns();
|
||||
|
||||
document.getElementById('page-title').textContent = `Прайслист ${pl.version}`;
|
||||
document.getElementById('pl-version').textContent = pl.version;
|
||||
@@ -128,13 +133,15 @@
|
||||
|
||||
const resp = await fetch(url);
|
||||
const data = await resp.json();
|
||||
currentSource = data.source || currentSource;
|
||||
toggleWarehouseColumns();
|
||||
|
||||
renderItems(data.items || []);
|
||||
renderItemsPagination(data.total, data.page, data.per_page);
|
||||
} catch (e) {
|
||||
document.getElementById('items-body').innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-4 text-center text-red-500">
|
||||
<td colspan="${itemsColspan()}" class="px-6 py-4 text-center text-red-500">
|
||||
Ошибка загрузки: ${e.message}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -142,6 +149,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isWarehouseSource() {
|
||||
return (currentSource || '').toLowerCase() === 'warehouse';
|
||||
}
|
||||
|
||||
function itemsColspan() {
|
||||
return isWarehouseSource() ? 7 : 5;
|
||||
}
|
||||
|
||||
function toggleWarehouseColumns() {
|
||||
const visible = isWarehouseSource();
|
||||
document.getElementById('th-qty').classList.toggle('hidden', !visible);
|
||||
document.getElementById('th-partnumbers').classList.toggle('hidden', !visible);
|
||||
}
|
||||
|
||||
function formatQty(qty) {
|
||||
if (typeof qty !== 'number') return '—';
|
||||
if (Number.isInteger(qty)) return qty.toString();
|
||||
return qty.toLocaleString('ru-RU', { minimumFractionDigits: 0, maximumFractionDigits: 3 });
|
||||
}
|
||||
|
||||
function formatPriceSettings(item) {
|
||||
// Format price settings to match admin pricing interface style
|
||||
let settings = [];
|
||||
@@ -188,7 +215,7 @@
|
||||
if (items.length === 0) {
|
||||
document.getElementById('items-body').innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-4 text-center text-gray-500">
|
||||
<td colspan="${itemsColspan()}" class="px-6 py-4 text-center text-gray-500">
|
||||
${searchQuery ? 'Ничего не найдено' : 'Позиции не найдены'}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -196,10 +223,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const showWarehouse = isWarehouseSource();
|
||||
const html = items.map(item => {
|
||||
const price = item.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
const description = item.lot_description || '-';
|
||||
const truncatedDesc = description.length > 60 ? description.substring(0, 60) + '...' : description;
|
||||
const qty = formatQty(item.available_qty);
|
||||
const partnumbers = Array.isArray(item.partnumbers) && item.partnumbers.length > 0 ? item.partnumbers.join(', ') : '—';
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-gray-50">
|
||||
@@ -210,6 +240,8 @@
|
||||
<span class="px-2 py-1 text-xs bg-gray-100 rounded">${item.category || '-'}</span>
|
||||
</td>
|
||||
<td class="px-6 py-3 text-sm text-gray-500" title="${description}">${truncatedDesc}</td>
|
||||
${showWarehouse ? `<td class="px-6 py-3 whitespace-nowrap text-right font-mono">${qty}</td>` : ''}
|
||||
${showWarehouse ? `<td class="px-6 py-3 text-sm text-gray-600" title="${escapeHtml(partnumbers)}">${escapeHtml(partnumbers)}</td>` : ''}
|
||||
<td class="px-6 py-3 whitespace-nowrap text-right font-mono">${price}</td>
|
||||
<td class="px-6 py-3 whitespace-nowrap text-sm"><span class="text-xs bg-gray-100 px-2 py-1 rounded">${formatPriceSettings(item)}</span></td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user