Refine stock import UX with suggestions, ignore rules, and inline mapping controls

This commit is contained in:
Mikhail Chusavitin
2026-02-06 19:58:42 +03:00
parent eb8ac34d83
commit 5f2969a85a
6 changed files with 766 additions and 147 deletions

View File

@@ -110,23 +110,55 @@
</div>
<div id="stock-import-stats" class="text-xs text-gray-600 mt-2"></div>
</div>
<div id="stock-import-suggestions" class="hidden mt-4 p-3 bg-amber-50 border border-amber-200 rounded">
<div class="flex items-center justify-between mb-2">
<div class="text-sm font-medium text-amber-900">Новые партномера без сопоставления</div>
<div class="flex items-center gap-2">
<button onclick="addAllSuggestions()" class="inline-flex items-center gap-1 px-2 py-1 text-xs rounded border border-blue-200 text-blue-700 hover:bg-blue-50" title="Добавить все">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span>Добавить все</span>
</button>
<button onclick="ignoreAllSuggestions()" class="inline-flex items-center gap-1 px-2 py-1 text-xs rounded border border-amber-200 text-amber-700 hover:bg-amber-100" title="Игнорировать все">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-12.728 12.728M5.636 5.636l12.728 12.728"></path>
</svg>
<span>Игнорировать все</span>
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-amber-200">
<thead class="bg-amber-100">
<tr>
<th class="px-3 py-2 w-1/4 text-left text-xs font-medium text-amber-800 uppercase">LOT</th>
<th class="px-3 py-2 w-1/4 text-left text-xs font-medium text-amber-800 uppercase">Partnumber</th>
<th class="px-3 py-2 text-left text-xs font-medium text-amber-800 uppercase">Описание</th>
<th class="px-3 py-2 text-left text-xs font-medium text-amber-800 uppercase">Причина</th>
<th class="px-3 py-2 text-right text-xs font-medium text-amber-800 uppercase">Действия</th>
</tr>
</thead>
<tbody id="stock-import-suggestions-body" class="bg-white divide-y divide-amber-100"></tbody>
</table>
</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>
<input id="stock-mappings-search" type="text" placeholder="Поиск по partnumber / описанию / lot" class="px-3 py-2 border rounded w-full">
<datalist id="stock-lot-options"></datalist>
<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 w-1/4 text-left text-xs font-medium text-gray-500 uppercase">LOT</th>
<th class="px-4 py-2 w-1/4 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>
@@ -137,6 +169,40 @@
</div>
<div id="stock-mappings-pagination" class="flex justify-center space-x-2 mt-3"></div>
</div>
<div class="border rounded-lg p-4">
<h3 class="text-lg font-semibold mb-3">Игнорирование при импорте</h3>
<div class="flex items-center gap-2 mb-3">
<select id="ignore-target" class="px-3 py-2 border rounded">
<option value="partnumber">Partnumber</option>
<option value="description">Описание</option>
</select>
<select id="ignore-match-type" class="px-3 py-2 border rounded">
<option value="exact">Равно</option>
<option value="prefix">Начинается с</option>
<option value="suffix">Заканчивается на</option>
</select>
<input id="ignore-pattern" type="text" placeholder="Шаблон" class="px-3 py-2 border rounded w-1/3">
<button onclick="saveStockIgnoreRule()" class="px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Добавить</button>
<button onclick="loadStockIgnoreRules(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">Поле</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">Шаблон</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase">Действия</th>
</tr>
</thead>
<tbody id="stock-ignore-rules-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-ignore-rules-pagination" class="flex justify-center space-x-2 mt-3"></div>
</div>
</div>
</div>
@@ -309,6 +375,10 @@ let cachedDbUsername = null;
let syncUsersStatusTimer = null;
let stockMappingsPage = 1;
let stockMappingsCache = [];
let stockIgnoreRulesPage = 1;
let stockMappingsSearch = '';
let stockMappingsSearchTimer = null;
let stockImportSuggestions = [];
async function loadTab(tab) {
currentTab = tab;
@@ -341,6 +411,8 @@ async function loadTab(tab) {
await loadPricelists(1, currentPricelistSource);
if (tab === 'warehouse') {
await loadStockMappings(1);
await loadStockIgnoreRules(1);
await loadStockLotOptions();
}
} else if (tab === 'component-settings') {
document.getElementById('search-bar').className = 'mb-4';
@@ -1046,7 +1118,9 @@ async function importStockFile() {
const percentEl = document.getElementById('stock-import-percent');
const barEl = document.getElementById('stock-import-bar');
const statsEl = document.getElementById('stock-import-stats');
const suggestionsBox = document.getElementById('stock-import-suggestions');
box.classList.remove('hidden');
suggestionsBox.classList.add('hidden');
statusEl.textContent = 'Запуск импорта...';
percentEl.textContent = '0%';
barEl.style.width = '0%';
@@ -1082,16 +1156,19 @@ async function importStockFile() {
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}`;
`Пропущено: ${(data.unmapped || 0) + (data.conflicts || 0) + (data.parse_errors || 0) + (data.ignored || 0)} | ` +
`Игнор: ${data.ignored || 0} | Конфликты: ${data.conflicts || 0}`;
if (data.status === 'error') {
throw new Error(data.message || 'Ошибка импорта');
}
if (data.status === 'completed') {
stockImportSuggestions = data.mapping_suggestions || [];
renderStockImportSuggestions(stockImportSuggestions);
showToast('Импорт stock_log завершен', 'success');
await loadPricelists(1, 'warehouse');
await loadStockMappings(stockMappingsPage);
await loadStockIgnoreRules(stockIgnoreRulesPage);
}
}
}
@@ -1100,13 +1177,151 @@ async function importStockFile() {
}
}
function formatSuggestionReason(reason) {
if (reason === 'conflict') return 'Конфликт';
return 'Не найден LOT';
}
function renderStockImportSuggestions(items) {
const box = document.getElementById('stock-import-suggestions');
const body = document.getElementById('stock-import-suggestions-body');
if (!box || !body) return;
if (!items || items.length === 0) {
box.classList.add('hidden');
body.innerHTML = '';
return;
}
body.innerHTML = items.map(item => `
<tr>
<td class="px-3 py-2 w-1/4 text-sm font-mono">
<input data-role="suggestion-lot" data-partnumber="${escapeHtml(item.partnumber || '')}" list="stock-lot-options" autocomplete="off" type="text" class="px-2 py-1 border rounded w-full font-mono" placeholder="Выберите LOT">
</td>
<td class="px-3 py-2 w-1/4 text-sm font-mono">${escapeHtml(item.partnumber || '')}</td>
<td class="px-3 py-2 text-sm text-gray-700">${escapeHtml(item.description || '—')}</td>
<td class="px-3 py-2 text-sm text-gray-700">${escapeHtml(formatSuggestionReason(item.reason || 'unmapped'))}</td>
<td class="px-3 py-2 text-right">
<div class="flex justify-end items-center gap-3">
<button data-partnumber="${escapeHtml(item.partnumber || '')}" onclick="addSuggestionMapping(this)" class="text-blue-600 hover:text-blue-800" title="Добавить">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" data-description="${encodeURIComponent(item.description || '')}">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</button>
<button data-partnumber="${escapeHtml(item.partnumber || '')}" onclick="ignoreSuggestion(this)" class="text-amber-600 hover:text-amber-800" title="Игнорировать">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-12.728 12.728M5.636 5.636l12.728 12.728"></path>
</svg>
</button>
</div>
</td>
</tr>
`).join('');
box.classList.remove('hidden');
}
async function addSuggestionMapping(button) {
const partnumber = (button?.dataset?.partnumber || '').trim();
if (!partnumber) return;
const description = decodeURIComponent(button?.querySelector('svg')?.dataset?.description || '');
const input = document.querySelector(`input[data-role="suggestion-lot"][data-partnumber="${CSS.escape(partnumber)}"]`);
const lotName = (input?.value || '').trim();
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, description: description })
});
const data = await resp.json();
if (!resp.ok) throw new Error(data.error || 'Ошибка сохранения');
stockImportSuggestions = stockImportSuggestions.filter(item => (item.partnumber || '').trim().toLowerCase() !== partnumber.toLowerCase());
renderStockImportSuggestions(stockImportSuggestions);
await loadStockMappings(1);
showToast('Сопоставление добавлено', 'success');
} catch (e) {
showToast('Ошибка: ' + e.message, 'error');
}
}
async function ignoreSuggestion(button) {
const partnumber = (button?.dataset?.partnumber || '').trim();
if (!partnumber) return;
try {
const resp = await fetch('/api/admin/pricing/stock/ignore-rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target: 'partnumber', match_type: 'exact', pattern: partnumber })
});
const data = await resp.json();
if (!resp.ok) throw new Error(data.error || 'Ошибка добавления в игнорирование');
stockImportSuggestions = stockImportSuggestions.filter(item => (item.partnumber || '').trim().toLowerCase() !== partnumber.toLowerCase());
renderStockImportSuggestions(stockImportSuggestions);
await loadStockIgnoreRules(1);
showToast('Добавлено в игнорирование', 'success');
} catch (e) {
showToast('Ошибка: ' + e.message, 'error');
}
}
async function addAllSuggestions() {
const descriptionByPartnumber = {};
for (const s of stockImportSuggestions) {
const pn = (s.partnumber || '').trim().toLowerCase();
if (!pn) continue;
descriptionByPartnumber[pn] = s.description || '';
}
const inputs = Array.from(document.querySelectorAll('input[data-role="suggestion-lot"]'));
const tasks = inputs
.map(input => ({
partnumber: (input.dataset.partnumber || '').trim(),
lot: (input.value || '').trim(),
description: descriptionByPartnumber[((input.dataset.partnumber || '').trim().toLowerCase())] || ''
}))
.filter(x => x.partnumber);
if (tasks.length === 0) {
showToast('Нет строк для добавления', 'error');
return;
}
let ok = 0;
for (const t of tasks) {
const resp = await fetch('/api/admin/pricing/stock/mappings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ partnumber: t.partnumber, lot_name: t.lot, description: t.description })
});
if (resp.ok) ok++;
}
stockImportSuggestions = stockImportSuggestions.filter(item => !tasks.some(t => t.partnumber.toLowerCase() === (item.partnumber || '').toLowerCase()));
renderStockImportSuggestions(stockImportSuggestions);
await loadStockMappings(1);
showToast(`Добавлено: ${ok}`, 'success');
}
async function ignoreAllSuggestions() {
if (!stockImportSuggestions.length) return;
let ok = 0;
for (const s of stockImportSuggestions) {
const partnumber = (s.partnumber || '').trim();
if (!partnumber) continue;
const resp = await fetch('/api/admin/pricing/stock/ignore-rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target: 'partnumber', match_type: 'exact', pattern: partnumber })
});
if (resp.ok) ok++;
}
stockImportSuggestions = [];
renderStockImportSuggestions(stockImportSuggestions);
await loadStockIgnoreRules(1);
showToast(`Добавлено в игнорирование: ${ok}`, 'success');
}
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 query = encodeURIComponent(stockMappingsSearch);
const resp = await fetch(`/api/admin/pricing/stock/mappings?page=${page}&per_page=20&search=${query}`);
const data = await resp.json();
if (!resp.ok) throw new Error(data.error || 'Ошибка загрузки');
const items = data.items || [];
@@ -1115,12 +1330,30 @@ async function loadStockMappings(page = 1) {
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 class="cursor-pointer hover:bg-gray-50" onclick="selectStockMappingRow('${escapeHtml((item.partnumber || item.Partnumber || '')).replace(/'/g, "\\'")}')">
<td class="px-4 py-2 text-sm font-mono">${escapeHtml(item.partnumber || item.Partnumber || '—')}</td>
<tr>
<td class="px-4 py-2 w-1/4 text-sm font-mono">
<input data-role="lot" type="text" list="stock-lot-options" autocomplete="off" placeholder="Выберите LOT" value="${escapeHtml(item.lot_name || item.LotName || '')}" class="px-2 py-1 border rounded w-full font-mono">
</td>
<td class="px-4 py-2 w-1/4 text-sm font-mono">${escapeHtml(item.partnumber || item.Partnumber || '—')}</td>
<td class="px-4 py-2 text-sm text-gray-600">${escapeHtml(item.description || item.Description || '—')}</td>
<td class="px-4 py-2 text-sm font-mono">${escapeHtml(item.lot_name || item.LotName || '—')}</td>
<td class="px-4 py-2 text-right">
<button data-partnumber="${escapeHtml(item.partnumber || item.Partnumber || '')}" onclick="event.stopPropagation(); deleteStockMapping(this.dataset.partnumber)" class="text-red-600 hover:text-red-800 text-sm">Удалить</button>
<td class="px-4 py-2">
<div class="flex justify-end items-center gap-3">
<button data-partnumber="${escapeHtml(item.partnumber || item.Partnumber || '')}" onclick="saveInlineStockMapping(this)" class="text-blue-600 hover:text-blue-800" title="Сохранить">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</button>
<button data-partnumber="${escapeHtml(item.partnumber || item.Partnumber || '')}" onclick="event.stopPropagation(); ignoreStockMapping(this.dataset.partnumber)" class="text-amber-600 hover:text-amber-800" title="Игнорировать">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-12.728 12.728M5.636 5.636l12.728 12.728"></path>
</svg>
</button>
<button data-partnumber="${escapeHtml(item.partnumber || item.Partnumber || '')}" onclick="event.stopPropagation(); deleteStockMapping(this.dataset.partnumber)" class="text-red-600 hover:text-red-800" title="Удалить">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 7h12M9 7V5a1 1 0 011-1h4a1 1 0 011 1v2m-7 0v12m4-12v12m4-12v12"></path>
</svg>
</button>
</div>
</td>
</tr>
`).join('');
@@ -1143,19 +1376,23 @@ async function loadStockMappings(page = 1) {
}
}
function selectStockMappingRow(partnumber) {
const normalized = (partnumber || '').trim().toLowerCase();
const row = stockMappingsCache.find(item => ((item.partnumber || item.Partnumber || '').trim().toLowerCase() === normalized));
if (!row) return;
document.getElementById('mapping-partnumber').value = (row.partnumber || row.Partnumber || '').trim();
document.getElementById('mapping-lotname').value = (row.lot_name || row.LotName || '').trim();
function applyStockMappingsSearch() {
stockMappingsSearch = (document.getElementById('stock-mappings-search')?.value || '').trim();
loadStockMappings(1);
}
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');
function onStockMappingsSearchInput() {
clearTimeout(stockMappingsSearchTimer);
stockMappingsSearchTimer = setTimeout(applyStockMappingsSearch, 250);
}
async function saveInlineStockMapping(button) {
const partnumber = (button?.dataset?.partnumber || '').trim();
if (!partnumber) return;
const row = button.closest('tr');
const lotName = (row?.querySelector('input[data-role="lot"]')?.value || '').trim();
if (!lotName) {
showToast('LOT не может быть пустым', 'error');
return;
}
try {
@@ -1166,10 +1403,103 @@ async function saveStockMapping() {
});
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);
await loadStockMappings(stockMappingsPage);
} catch (e) {
showToast('Ошибка: ' + e.message, 'error');
}
}
async function loadStockLotOptions() {
const datalist = document.getElementById('stock-lot-options');
if (!datalist) return;
try {
const resp = await fetch('/api/admin/pricing/lots?per_page=5000');
const data = await resp.json();
if (!resp.ok) throw new Error(data.error || 'Ошибка загрузки LOT');
const items = data.items || [];
datalist.innerHTML = items.map(lot => `<option value="${escapeHtml(lot)}"></option>`).join('');
} catch (_) {
datalist.innerHTML = '';
}
}
async function loadStockIgnoreRules(page = 1) {
stockIgnoreRulesPage = page;
const body = document.getElementById('stock-ignore-rules-body');
const pagination = document.getElementById('stock-ignore-rules-pagination');
if (!body || !pagination) return;
try {
const resp = await fetch(`/api/admin/pricing/stock/ignore-rules?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 {
const matchLabel = { exact: 'Равно', prefix: 'Начинается с', suffix: 'Заканчивается на' };
body.innerHTML = items.map(item => `
<tr>
<td class="px-4 py-2 text-sm">${escapeHtml(item.target)}</td>
<td class="px-4 py-2 text-sm">${escapeHtml(matchLabel[item.match_type] || item.match_type)}</td>
<td class="px-4 py-2 text-sm font-mono">${escapeHtml(item.pattern)}</td>
<td class="px-4 py-2 text-right">
<button onclick="deleteStockIgnoreRule(${item.id})" 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="loadStockIgnoreRules(${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 saveStockIgnoreRule() {
const target = document.getElementById('ignore-target').value;
const matchType = document.getElementById('ignore-match-type').value;
const pattern = document.getElementById('ignore-pattern').value.trim();
if (!pattern) {
showToast('Заполните шаблон', 'error');
return;
}
try {
const resp = await fetch('/api/admin/pricing/stock/ignore-rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target: target, match_type: matchType, pattern: pattern })
});
const data = await resp.json();
if (!resp.ok) throw new Error(data.error || 'Ошибка сохранения');
document.getElementById('ignore-pattern').value = '';
showToast('Правило добавлено', 'success');
await loadStockIgnoreRules(1);
} catch (e) {
showToast('Ошибка: ' + e.message, 'error');
}
}
async function deleteStockIgnoreRule(id) {
if (!confirm('Удалить правило игнорирования?')) return;
try {
const resp = await fetch(`/api/admin/pricing/stock/ignore-rules/${id}`, {
method: 'DELETE'
});
const data = await resp.json();
if (!resp.ok) throw new Error(data.error || 'Ошибка удаления');
showToast('Правило удалено', 'success');
await loadStockIgnoreRules(stockIgnoreRulesPage);
} catch (e) {
showToast('Ошибка: ' + e.message, 'error');
}
@@ -1190,6 +1520,33 @@ async function deleteStockMapping(partnumber) {
}
}
async function ignoreStockMapping(partnumber) {
const pn = (partnumber || '').trim();
if (!pn) return;
if (!confirm('Добавить партномер в игнорирование и удалить из сопоставлений?')) return;
try {
const addResp = await fetch('/api/admin/pricing/stock/ignore-rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target: 'partnumber', match_type: 'exact', pattern: pn })
});
const addData = await addResp.json();
if (!addResp.ok) throw new Error(addData.error || 'Ошибка добавления в игнорирование');
const delResp = await fetch(`/api/admin/pricing/stock/mappings/${encodeURIComponent(pn)}`, {
method: 'DELETE'
});
const delData = await delResp.json();
if (!delResp.ok) throw new Error(delData.error || 'Ошибка удаления сопоставления');
showToast('Партномер добавлен в игнорирование', 'success');
await loadStockMappings(stockMappingsPage);
await loadStockIgnoreRules(1);
} catch (e) {
showToast('Ошибка: ' + e.message, 'error');
}
}
document.addEventListener('DOMContentLoaded', async () => {
await checkPricelistWritePermission();
// Check URL params for initial tab
@@ -1198,6 +1555,10 @@ document.addEventListener('DOMContentLoaded', async () => {
if (initialTab === 'pricelists') initialTab = 'estimate';
if (initialTab === 'components') initialTab = 'component-settings';
await loadTab(initialTab);
const stockMappingsSearchEl = document.getElementById('stock-mappings-search');
if (stockMappingsSearchEl) {
stockMappingsSearchEl.addEventListener('input', onStockMappingsSearchInput);
}
// Add event listeners for preview updates
document.getElementById('modal-period').addEventListener('change', fetchPreview);