Формат:
Артикул
Название / Бренд
SKU OZON
Баркод
Кол-во
Нажмите «Загрузить товары»
Шаблон:

Предпросмотр 0

Выберите товары слева →
Этикетки появятся здесь
`; const win = window.open('', '_blank'); win.document.write(html); win.document.close(); setTimeout(() => { win.focus(); win.print(); }, 800); } function removeLabel(labelIdx) { // Найти и снять чекбокс у соответствующего товара let li = 0; for (let idx = 0; idx < filteredItems.length; idx++) { const item = filteredItems[idx]; const itemKey = getItemKey(item); if (selected[itemKey]) { if (li === labelIdx) { delete selected[itemKey]; break; } li += selected[itemKey]; } } renderTable(); rebuildPreview(); } function clearPreview() { clearAll(); } // ===== РЕЖИМ ВКЛАДОК ===== let currentMode = 'quick'; function setMode(mode) { currentMode = mode; const quickTab = document.getElementById('modeTabQuick'); const dbTab = document.getElementById('modeTabDb'); if (quickTab) quickTab.classList.toggle('active', mode==='quick'); if (dbTab) dbTab.classList.toggle('active', mode==='db'); document.getElementById('toolbarQuick').style.display = mode==='quick' ? '' : 'none'; document.getElementById('toolbarDb').style.display = mode==='db' ? '' : 'none'; document.querySelector('.main-layout').style.display = mode==='quick' ? 'flex' : 'none'; document.getElementById('dbView').style.display = mode==='db' ? 'flex' : 'none'; if (mode==='db') loadDbItems(); } // ===== БАЗА ЭТИКЕТОК ===== let dbItems = [], dbFiltered = [], dbSelected = new Set(); async function loadDbItems(force) { document.getElementById('dbBody').innerHTML = '⏳ Загружаем...'; try { const r = await fetch(`${API_URL}/api/label-items`); const j = await r.json(); dbItems = j.items || []; filterDbItems(); } catch(e) { document.getElementById('dbBody').innerHTML = 'Ошибка загрузки'; } } function filterDbItems() { const q = (document.getElementById('dbSearch').value||'').toLowerCase().trim(); dbFiltered = !q ? dbItems : dbItems.filter(it => (it.article||'').toLowerCase().includes(q) || (it.title||'').toLowerCase().includes(q) || (it.barcode||'').includes(q) || (it.brand||'').toLowerCase().includes(q) || (it.size||'').toLowerCase().includes(q) || (it.template_name||'').toLowerCase().includes(q) ); renderDbTable(); } function renderDbTable() { const body = document.getElementById('dbBody'); if (!dbFiltered.length) { body.innerHTML='Нет записей'; return; } body.innerHTML = dbFiltered.map(it => { const chk = dbSelected.has(it.id); const date = (it.created_at||'').substring(0,16); const printed = it.printed ? '✓ Да' : ''; return ` ${it.article||'—'} ${it.title||'—'} ${it.brand||'—'} ${it.size||'—'} ${it.barcode||'—'} ${it.template_name||'нет'} ${printed} ${date} `; }).join(''); updateDbChkAll(); } function dbRowClick(id, e) { if (e.target.tagName==='INPUT'||e.target.tagName==='BUTTON') return; dbToggle(id, !dbSelected.has(id)); renderDbTable(); showDbPreview(id); } function dbToggle(id, val) { if (val) dbSelected.add(id); else dbSelected.delete(id); updateDbChkAll(); } function toggleAllDb(checked) { if (checked) dbFiltered.forEach(it=>dbSelected.add(it.id)); else dbSelected.clear(); renderDbTable(); } function selectAllDb() { dbFiltered.forEach(it=>dbSelected.add(it.id)); renderDbTable(); } function clearAllDb() { dbSelected.clear(); renderDbTable(); document.getElementById('dbPreviewArea').innerHTML='
Выберите запись
'; } function updateDbChkAll() { const chk = document.getElementById('dbChkAll'); if (!dbFiltered.length) { chk.checked=false; chk.indeterminate=false; return; } const cnt = dbFiltered.filter(it=>dbSelected.has(it.id)).length; chk.checked = cnt===dbFiltered.length; chk.indeterminate = cnt>0 && cnti.id===id); if (!it) return; it.qty = qty; await fetch(`${API_URL}/api/label-items/${id}`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({...it, qty}) }); } async function deleteDbItem(e, id) { e.stopPropagation(); if (!confirm('Удалить запись?')) return; await fetch(`${API_URL}/api/label-items/${id}`, {method:'DELETE'}); dbSelected.delete(id); await loadDbItems(); } async function deleteSelectedDb() { const ids = [...dbSelected]; if (!ids.length || !confirm(`Удалить ${ids.length} записей?`)) return; await fetch(`${API_URL}/api/label-items/delete-batch`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ids}) }); dbSelected.clear(); await loadDbItems(); } // ===== ПРЕДПРОСМОТР ЗАПИСИ ИЗ БАЗЫ ===== function showDbPreview(id) { const it = dbItems.find(i=>i.id===id); if (!it || !it.design_json) { document.getElementById('dbPreviewArea').innerHTML = '
Шаблон не найден — нет превью
'; return; } const data = { barcode:it.barcode, article:it.article, title:it.title, brand:it.brand, size:it.size, nmId:it.nm_id }; const design = JSON.parse(it.design_json||'{}'); const elems = design.elements || []; const lw = it.label_w || 58, lh = it.label_h || 40; const zoom = Math.min(300/lw, 200/lh, 3); const MM = 3.7795; let html = `
`; [...elems].reverse().forEach(e => { const subst = t => (t||'').replace(/\{\{(\w+)\}\}/g, (_,v)=>data[v]||''); let inner='', css=`position:absolute;left:${e.x*MM*zoom}px;top:${e.y*MM*zoom}px;width:${e.w*MM*zoom}px;height:${e.h*MM*zoom}px;overflow:${e.type==='barcode'?'visible':'hidden'};box-sizing:border-box;opacity:${(e.opacity||100)/100};`; if (e.type==='text') { css+=`display:flex;flex-direction:column;justify-content:${getFlexAlign(getTextVAlign(e))};font-size:${getTextFontPt(e)*PT_TO_PX*zoom}px;font-family:${e.fontFamily};font-weight:${e.bold?'bold':'normal'};font-style:${e.italic?'italic':'normal'};color:${e.color};text-align:${e.align};line-height:${e.lineHeight};${getTextWrapCss(e)}`; inner = escHtml(addWordWrapHints(subst(e.content), e)); } else if (e.type==='rect') { css+=`background:${e.fillColor==='transparent'?'transparent':e.fillColor};border:${e.borderWidth*MM*zoom}px solid ${e.borderColor};border-radius:${e.borderRadius*MM*zoom}px;`; } else if (e.type==='line') { inner=`
`; } else if (e.type==='barcode') { const val = subst(e.value)||'0000000000'; const svg = document.createElementNS('http://www.w3.org/2000/svg','svg'); try { JsBarcode(svg, val, {format:e.format||'CODE128',width:e.barWidth*zoom*0.6,height:(e.h-3)*MM*zoom,displayValue:e.showText,fontSize:getBarcodeFontPt(e)*PT_TO_PX*zoom,margin:0,background:'transparent',lineColor:e.color||'#000'}); } catch(err){} css+='display:flex;align-items:center;justify-content:center;overflow:visible;'; inner=svg.outerHTML; } html+=`
${inner}
`; }); html+='
'; document.getElementById('dbPreviewArea').innerHTML = html; } // ===== ПЕЧАТЬ ИЗ БАЗЫ ===== async function printSelectedDb() { const ids = [...dbSelected]; if (!ids.length) { alert('Выберите записи для печати'); return; } const items = dbFiltered.filter(it=>dbSelected.has(it.id)); // Группируем по шаблону const byTemplate = {}; items.forEach(it => { const key = it.template_id || '__none__'; if (!byTemplate[key]) byTemplate[key] = { design_json:it.design_json, label_w:it.label_w||58, label_h:it.label_h||40, items:[] }; byTemplate[key].items.push(it); }); let allLabelsHTML = ''; const MM_str = val => Math.round(val*100)/100; Object.values(byTemplate).forEach(({ design_json, label_w, label_h, items }) => { const design = JSON.parse(design_json||'{}'); const elems = design.elements || []; items.forEach(it => { const data = { barcode:it.barcode, article:it.article, title:it.title, brand:it.brand, size:it.size, nmId:it.nm_id }; const subst = t => (t||'').replace(/\{\{(\w+)\}\}/g, (_,v)=>data[v]||''); const copies = it.qty || 1; const elemsCSS = elems.filter(e=>e.visible!==false).map(e => { let inner='', css=`position:absolute;left:${MM_str(e.x)}mm;top:${MM_str(e.y)}mm;width:${MM_str(e.w)}mm;height:${MM_str(e.h)}mm;overflow:${e.type==='barcode'?'visible':'hidden'};box-sizing:border-box;opacity:${(e.opacity||100)/100};`; if (e.type==='text') { css+=`display:flex;flex-direction:column;justify-content:${getFlexAlign(getTextVAlign(e))};font-size:${MM_str(getTextFontPt(e))}pt;font-family:${e.fontFamily};font-weight:${e.bold?'bold':'normal'};font-style:${e.italic?'italic':'normal'};text-decoration:${e.underline?'underline':'none'};color:${e.color};text-align:${e.align};line-height:${e.lineHeight};${getTextWrapCss(e)}`; inner = escHtml(addWordWrapHints(subst(e.content), e)); } else if (e.type==='rect') { inner=`
`; } else if (e.type==='line') { css+='display:flex;align-items:center;'; inner=`
`; } else if (e.type==='barcode') { const val=subst(e.value)||'0000000000'; const svg=document.createElementNS('http://www.w3.org/2000/svg','svg'); try{JsBarcode(svg,val,{format:e.format||'CODE128',width:e.barWidth*1.5,height:(e.h-3)*3.7795,displayValue:e.showText,fontSize:getBarcodeFontPt(e)*PT_TO_PX,margin:0,background:'transparent',lineColor:e.color||'#000'});}catch(err){} inner=`
${svg.outerHTML}
`; } return `
${inner}
`; }).join(''); for (let i=0;i${elemsCSS}`; } }); }); const win = window.open('','_blank'); win.document.write(` ${allLabelsHTML}`); win.document.close(); setTimeout(()=>{win.focus();win.print();},600); // Помечаем как напечатанные fetch(`${API_URL}/api/label-items/mark-printed`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids})}); } async function syncAndReload() { try { if (typeof currentOrgId !== 'undefined' && currentOrgId && typeof triggerOzonOrgSync === 'function') { await triggerOzonOrgSync(); } else { await fetch(`${API_URL}/api/ozon/sync`, { method: 'POST' }); } setTimeout(() => loadProducts(true), 5000); } catch (e) { console.error('syncAndReload:', e); alert('Не удалось запустить синхронизацию OZON'); } } async function pollNewProducts() { if (productsPollInFlight || document.hidden) return; productsPollInFlight = true; try { const op = currentOrgId ? `?org_id=${currentOrgId}` : ''; const data = await cachedFetch(`${API_URL}/api/ozon/labels/products${op}`, true); if (data.success) { const incoming = (data.items || []).map(mapOzonLabelItem); const newItems = incoming.filter(item => !knownItemKeys.has(getItemKey(item))); if (newItems.length) { newItems.forEach(item => sessionNewItemKeys.add(getItemKey(item))); allItems = sortItemsWithNewFirst(incoming); syncKnownItemKeys(); persistProductsToSession(); applyFilters(); showNewItemsToast(newItems); } else if (incoming.length) { allItems = sortItemsWithNewFirst(incoming); syncKnownItemKeys(); persistProductsToSession(); applyFilters(); } } } catch (e) { console.warn('pollNewProducts:', e); } finally { productsPollInFlight = false; } } function startProductsPolling() { if (productsPollTimer) clearInterval(productsPollTimer); productsPollTimer = setInterval(pollNewProducts, PRODUCTS_POLL_MS); } async function initProductsPage() { await initOzonCabinetContext(); loadQuickColumnWidths(); loadTemplates(); const restored = restoreProductsFromSession(); if (restored) applyFilters(); else await loadProducts(false); if (!localStorage.getItem(getQuickColsStorageKey()) && allItems.length) { autoFitQuickColumns(); } else { applyQuickColumnWidths(); } startProductsPolling(); setTimeout(() => pollNewProducts(), restored ? 5000 : 15000); } document.addEventListener('visibilitychange', () => { if (!document.hidden) pollNewProducts(); }); window.addEventListener('beforeunload', () => { if (productsPollTimer) clearInterval(productsPollTimer); }); document.addEventListener('mousemove', e => { if (!quickResizeState) return; const minWidth = quickResizeState.colIdx === 0 ? 34 : 60; quickColWidths[quickResizeState.colIdx] = Math.max(minWidth, quickResizeState.startW + (e.clientX - quickResizeState.startX)); applyQuickColumnWidths(); }); document.addEventListener('mouseup', () => { if (!quickResizeState) return; document.querySelector(`.col-resizer[data-col="${quickResizeState.colIdx}"]`)?.classList.remove('active'); saveQuickColumnWidths(); quickResizeState = null; }); // Загружаем шаблоны и товары при старте initProductsPage(); // ===== ФОРМА "ДОБАВИТЬ ВРУЧНУЮ" ===== let manualBarcodes = []; function openManualForm() { manualBarcodes = []; document.getElementById('mfArticle').value = ''; document.getElementById('mfTitle').value = ''; document.getElementById('mfBrand').value = ''; document.getElementById('mfSize').value = ''; document.getElementById('mfColor').value = ''; document.getElementById('mfBarcode').value = ''; document.getElementById('mfQty').value = '1'; renderManualBarcodes(); document.getElementById('manualOverlay').classList.add('open'); document.getElementById('mfArticle').focus(); } function closeManualForm() { document.getElementById('manualOverlay').classList.remove('open'); } function addManualBarcode() { const inp = document.getElementById('mfBarcode'); const val = inp.value.trim(); if (!val) return; if (!manualBarcodes.includes(val)) manualBarcodes.push(val); inp.value = ''; renderManualBarcodes(); } function removeManualBarcode(bc) { manualBarcodes = manualBarcodes.filter(b => b !== bc); renderManualBarcodes(); } function renderManualBarcodes() { const list = document.getElementById('mfBarcodesList'); list.innerHTML = manualBarcodes.map(bc => `${bc}` ).join(''); } function submitManualForm() { const article = document.getElementById('mfArticle').value.trim(); const title = document.getElementById('mfTitle').value.trim(); const brand = document.getElementById('mfBrand').value.trim(); const size = document.getElementById('mfSize').value.trim(); const color = document.getElementById('mfColor').value.trim(); const qty = Math.max(1, Math.min(99, +document.getElementById('mfQty').value || 1)); // Также добавить текущий незавершённый баркод из поля const inpBc = document.getElementById('mfBarcode').value.trim(); if (inpBc && !manualBarcodes.includes(inpBc)) manualBarcodes.push(inpBc); if (!article && !manualBarcodes.length) { alert('Введите хотя бы артикул или баркод'); return; } const primaryBc = manualBarcodes[0] || ''; const item = { barcode: primaryBc, barcodes: [...manualBarcodes], sellerSku: article, title, brand, size, color, category: '', nmId: '' }; previewLabels.push({ item, qty }); document.getElementById('labelCount').textContent = previewLabels.reduce((s, l) => s + l.qty, 0); renderPreview(); closeManualForm(); } // Enter в поле баркода → добавить баркод function bindManualBarcodeEnter(){ const barcodeInput = document.getElementById('mfBarcode'); if (!barcodeInput || barcodeInput.dataset.enterBound === '1') return; barcodeInput.dataset.enterBound = '1'; barcodeInput.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); addManualBarcode(); } }); } bindManualBarcodeEnter(); document.addEventListener('DOMContentLoaded', bindManualBarcodeEnter);
✏️ Добавить этикетку вручную