Vendor mapping: wildcard ignore patterns, bulk CSV import, multi-lot qty
- Add glob pattern support (* and ?) for ignore rules stored in qt_vendor_partnumber_seen (is_pattern flag, migration 041) - Pattern matching applied in stock/competitor import, partnumber book snapshot, and vendor mappings list (Go-side via NormalizeKey) - BulkUpsertMappings: replace N+1 loop with two batch SQL upserts, validating all lots in a single query (~1500 queries → 3-4) - CSV import: multi-lot per PN via repeated rows, optional qty column - CSV export: updated column format vendor;partnumber;lot_name;qty;description;ignore;notes - UI: ignore patterns section with add/delete, import progress feedback - Update bible-local/vendor-mapping.md with new CSV format Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -690,7 +690,11 @@ async function importVendorMappingsCsv() {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
setVendorMappingsImportModalError('');
|
||||
if (submitBtn) submitBtn.disabled = true;
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Импортирую…';
|
||||
}
|
||||
if (typeof showToast === 'function') showToast('Импорт начался…', 'info');
|
||||
try {
|
||||
const resp = await fetch('/api/admin/pricing/vendor-mappings/import-csv', {
|
||||
method: 'POST',
|
||||
@@ -725,7 +729,10 @@ async function importVendorMappingsCsv() {
|
||||
} finally {
|
||||
input.value = '';
|
||||
renderVendorMappingsImportSelectedFile();
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Импортировать';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -891,4 +898,100 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
loadPartnumberBooks();
|
||||
loadVendorMappings(1);
|
||||
loadIgnorePatterns();
|
||||
});
|
||||
|
||||
// ─── Ignore Patterns ──────────────────────────────────────────────────────────
|
||||
|
||||
async function loadIgnorePatterns() {
|
||||
const container = document.getElementById('ignore-patterns-list');
|
||||
if (!container) return;
|
||||
try {
|
||||
const resp = await fetch('/api/admin/pricing/vendor-mappings/ignore-patterns');
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
const patterns = data.patterns || [];
|
||||
if (patterns.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-400 text-xs">Нет активных масок.</p>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = patterns.map(p => `
|
||||
<div class="flex items-center justify-between px-3 py-1.5 bg-gray-50 border rounded">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-mono text-sm font-medium text-orange-700">${escapeHtml(p.pattern)}</span>
|
||||
${p.ignored_by ? `<span class="text-xs text-gray-400">by ${escapeHtml(p.ignored_by)}</span>` : ''}
|
||||
</div>
|
||||
<button onclick="deleteIgnorePattern(${p.id}, ${JSON.stringify(p.pattern)})"
|
||||
class="text-xs text-red-500 hover:text-red-700 px-2 py-0.5 border border-red-200 rounded hover:bg-red-50">
|
||||
Удалить
|
||||
</button>
|
||||
</div>`).join('');
|
||||
} catch (e) {
|
||||
container.innerHTML = `<p class="text-red-500 text-xs">${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function openIgnorePatternsAddModal() {
|
||||
const modal = document.getElementById('ignore-pattern-modal');
|
||||
const inp = document.getElementById('ip-pattern');
|
||||
const err = document.getElementById('ignore-pattern-modal-error');
|
||||
if (inp) inp.value = '';
|
||||
if (err) { err.classList.add('hidden'); err.textContent = ''; }
|
||||
if (modal) { modal.classList.remove('hidden'); modal.classList.add('flex'); }
|
||||
setTimeout(() => inp && inp.focus(), 50);
|
||||
}
|
||||
|
||||
function closeIgnorePatternsAddModal() {
|
||||
const modal = document.getElementById('ignore-pattern-modal');
|
||||
if (modal) { modal.classList.add('hidden'); modal.classList.remove('flex'); }
|
||||
}
|
||||
|
||||
async function createIgnorePattern() {
|
||||
const inp = document.getElementById('ip-pattern');
|
||||
const err = document.getElementById('ignore-pattern-modal-error');
|
||||
const pattern = (inp?.value || '').trim();
|
||||
if (!pattern) { showIgnorePatternError('Введите маску'); return; }
|
||||
if (!/[*?]/.test(pattern)) { showIgnorePatternError('Маска должна содержать * или ?'); return; }
|
||||
try {
|
||||
const resp = await fetch('/api/admin/pricing/vendor-mappings/ignore-patterns', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pattern }),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) { showIgnorePatternError(data.error || 'Ошибка'); return; }
|
||||
closeIgnorePatternsAddModal();
|
||||
await loadIgnorePatterns();
|
||||
if (typeof showToast === 'function') showToast(`Маска «${pattern}» добавлена`, 'success');
|
||||
} catch (e) {
|
||||
showIgnorePatternError(e.message || 'Ошибка');
|
||||
}
|
||||
}
|
||||
|
||||
function showIgnorePatternError(msg) {
|
||||
const box = document.getElementById('ignore-pattern-modal-error');
|
||||
if (!box) return;
|
||||
box.textContent = msg;
|
||||
box.classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function deleteIgnorePattern(id, pattern) {
|
||||
if (!confirm(`Удалить маску «${pattern}»?`)) return;
|
||||
try {
|
||||
const resp = await fetch('/api/admin/pricing/vendor-mappings/ignore-patterns', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id }),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) { if (typeof showToast === 'function') showToast(data.error || 'Ошибка', 'error'); return; }
|
||||
await loadIgnorePatterns();
|
||||
if (typeof showToast === 'function') showToast(`Маска «${pattern}» удалена`, 'success');
|
||||
} catch (e) {
|
||||
if (typeof showToast === 'function') showToast(e.message || 'Ошибка', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user