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:
2026-02-06 23:30:01 +03:00
parent 5f2969a85a
commit 72ff842f5d
2 changed files with 72 additions and 53 deletions

View File

@@ -496,11 +496,18 @@ func (s *StockImportService) UpsertIgnoreRule(target, matchType, pattern string)
if target == "" || matchType == "" || pattern == "" {
return fmt.Errorf("target, match_type and pattern are required")
}
return s.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&models.StockIgnoreRule{
res := s.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&models.StockIgnoreRule{
Target: target,
MatchType: matchType,
Pattern: pattern,
}).Error
})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return fmt.Errorf("rule already exists")
}
return nil
}
func (s *StockImportService) DeleteIgnoreRule(id uint) (int64, error) {

View File

@@ -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,6 +1261,10 @@ async function ignoreSuggestion(button) {
}
async function addAllSuggestions() {
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();
@@ -1293,10 +1296,17 @@ async function addAllSuggestions() {
renderStockImportSuggestions(stockImportSuggestions);
await loadStockMappings(1);
showToast(`Добавлено: ${ok}`, 'success');
} finally {
if (btn) btn.disabled = false;
}
}
async function ignoreAllSuggestions() {
if (!stockImportSuggestions.length) return;
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();
@@ -1312,6 +1322,9 @@ async function ignoreAllSuggestions() {
renderStockImportSuggestions(stockImportSuggestions);
await loadStockIgnoreRules(1);
showToast(`Добавлено в игнорирование: ${ok}`, 'success');
} finally {
if (btn) btn.disabled = false;
}
}
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 {