Fix stock import UI bugs: dead code, fragile data attr, double-click, silent duplicates
- Remove unused stockMappingsCache variable (dead code after selectStockMappingRow removal) - Move data-description from SVG to button element for reliable access - Add disabled guard on bulk add/ignore buttons to prevent duplicate requests - Return explicit error in UpsertIgnoreRule when rule already exists Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -114,13 +114,13 @@
|
||||
<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="Добавить все">
|
||||
<button id="btn-add-all-suggestions" 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="Игнорировать все">
|
||||
<button id="btn-ignore-all-suggestions" 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>
|
||||
@@ -374,7 +374,6 @@ let isCreatingPricelist = false;
|
||||
let cachedDbUsername = null;
|
||||
let syncUsersStatusTimer = null;
|
||||
let stockMappingsPage = 1;
|
||||
let stockMappingsCache = [];
|
||||
let stockIgnoreRulesPage = 1;
|
||||
let stockMappingsSearch = '';
|
||||
let stockMappingsSearchTimer = null;
|
||||
@@ -1201,8 +1200,8 @@ function renderStockImportSuggestions(items) {
|
||||
<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 || '')}">
|
||||
<button data-partnumber="${escapeHtml(item.partnumber || '')}" data-description="${encodeURIComponent(item.description || '')}" 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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
@@ -1221,7 +1220,7 @@ function renderStockImportSuggestions(items) {
|
||||
async function addSuggestionMapping(button) {
|
||||
const partnumber = (button?.dataset?.partnumber || '').trim();
|
||||
if (!partnumber) return;
|
||||
const description = decodeURIComponent(button?.querySelector('svg')?.dataset?.description || '');
|
||||
const description = decodeURIComponent(button?.dataset?.description || '');
|
||||
const input = document.querySelector(`input[data-role="suggestion-lot"][data-partnumber="${CSS.escape(partnumber)}"]`);
|
||||
const lotName = (input?.value || '').trim();
|
||||
try {
|
||||
@@ -1262,56 +1261,70 @@ async function ignoreSuggestion(button) {
|
||||
}
|
||||
|
||||
async function addAllSuggestions() {
|
||||
const descriptionByPartnumber = {};
|
||||
for (const s of stockImportSuggestions) {
|
||||
const pn = (s.partnumber || '').trim().toLowerCase();
|
||||
if (!pn) continue;
|
||||
descriptionByPartnumber[pn] = s.description || '';
|
||||
const btn = document.getElementById('btn-add-all-suggestions');
|
||||
if (btn?.disabled) return;
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
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');
|
||||
} finally {
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
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++;
|
||||
const btn = document.getElementById('btn-ignore-all-suggestions');
|
||||
if (btn?.disabled) return;
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
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');
|
||||
} finally {
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
stockImportSuggestions = [];
|
||||
renderStockImportSuggestions(stockImportSuggestions);
|
||||
await loadStockIgnoreRules(1);
|
||||
showToast(`Добавлено в игнорирование: ${ok}`, 'success');
|
||||
}
|
||||
|
||||
async function loadStockMappings(page = 1) {
|
||||
@@ -1325,7 +1338,6 @@ async function loadStockMappings(page = 1) {
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data.error || 'Ошибка загрузки');
|
||||
const items = data.items || [];
|
||||
stockMappingsCache = items;
|
||||
if (items.length === 0) {
|
||||
body.innerHTML = '<tr><td colspan="4" class="px-4 py-3 text-sm text-gray-500">Нет сопоставлений</td></tr>';
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user