| |
| |
| |
| (function (global) { |
| 'use strict'; |
|
|
| const LS_KEY_LEGACY = 'alm-rep-reports'; |
| const LS_SIDEBAR_KEY = 'alm-rep-sidebar-collapsed'; |
| let deps = null; |
| let currentUser = null; |
| let initWired = false; |
| let store = { activeId: null, reports: [] }; |
| let ui = { |
| view: 'work', |
| segFilter: 'all', |
| tableSearch: '', |
| sidebarSearch: '', |
| saveMode: 'local', |
| serverAvailable: false, |
| sidebarCollapsed: false, |
| collecting: false, |
| lastWorkActiveId: null, |
| archivePreviewId: null, |
| tableSortK: null, |
| tableSortAsc: true, |
| }; |
|
|
| let pickPending = null; |
| let deletePendingId = null; |
| let storeRev = 0; |
| let serverTimer = null; |
| let saveInFlight = false; |
| let saveQueued = false; |
| let saveStatus = 'idle'; |
|
|
| const COLS = 10; |
|
|
| const ICON_ARCHIVE = |
| '<svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="12" height="3" rx=".5"/><path d="M3 5v7.5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5"/><path d="M6.5 8h3"/></svg>'; |
| const ICON_TRASH = |
| '<svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 4.5h11"/><path d="M5.5 4.5V3.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v1"/><path d="M5 4.5l.6 8a1 1 0 0 0 1 .9h2.8a1 1 0 0 0 1-.9l.6-8"/><path d="M6.5 7.2v4"/><path d="M9.5 7.2v4"/></svg>'; |
| const ICON_RESTORE = |
| '<svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="12" height="3" rx=".5"/><path d="M3 5v7.5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5"/><path d="M8 11V7"/><path d="M6 9l2-2 2 2"/></svg>'; |
|
|
| function val(k) { |
| const v = deps[k]; |
| return typeof v === 'function' ? v() : v; |
| } |
|
|
| function esc(s) { |
| return deps.escHtml(String(s ?? '')); |
| } |
|
|
| function attrEsc(s) { |
| return String(s ?? '').replace(/&/g, '&').replace(/"/g, '"'); |
| } |
|
|
| function migrateItem(ri) { |
| if (!ri.decision) { |
| if (ri.status === 'ok') ri.decision = 'approved'; |
| else if (ri.status === 'rejected') ri.decision = 'rejected'; |
| else ri.decision = 'pending'; |
| } |
| delete ri.status; |
| delete ri.alignMsg; |
| delete ri.checkedAt; |
| return ri; |
| } |
|
|
| function migrateReport(r) { |
| if (!r.status) r.status = r.savedAt ? 'saved' : 'draft'; |
| if (r.loaded === undefined) r.loaded = !!(r.items && r.items.length); |
| for (const it of r.items || []) migrateItem(it); |
| return r; |
| } |
|
|
| function migrateStore() { |
| for (const r of store.reports) migrateReport(r); |
| } |
|
|
| function lsKey(user) { |
| const u = (user || currentUser || '').trim().toLowerCase(); |
| return u ? `alm-rep-reports-${u}` : LS_KEY_LEGACY; |
| } |
|
|
| function apiFetchOpts(extra) { |
| const base = extra || {}; |
| const headers = global.almAuthHeaders |
| ? global.almAuthHeaders(base.headers || {}) |
| : { ...(base.headers || {}) }; |
| return { credentials: 'include', ...base, headers }; |
| } |
|
|
| function loadLocal(user) { |
| const u = (user || currentUser || '').trim().toLowerCase(); |
| if (!u) return; |
| currentUser = u; |
| try { |
| const raw = localStorage.getItem(lsKey(u)); |
| if (raw) { |
| const parsed = JSON.parse(raw); |
| if (parsed && Array.isArray(parsed.reports)) { |
| store = { activeId: null, reports: parsed.reports }; |
| migrateStore(); |
| } |
| } |
| } catch (_) {} |
| } |
|
|
| function saveLocal() { |
| if (!currentUser) return; |
| try { |
| localStorage.setItem( |
| lsKey(currentUser), |
| JSON.stringify({ activeId: null, reports: store.reports }) |
| ); |
| } catch (_) {} |
| } |
|
|
| function maybeImportLegacyLocal() { |
| if (store.reports.length) return; |
| try { |
| const raw = localStorage.getItem(LS_KEY_LEGACY); |
| if (!raw) return; |
| const parsed = JSON.parse(raw); |
| if (!parsed?.reports?.length) return; |
| store.reports = parsed.reports; |
| migrateStore(); |
| localStorage.removeItem(LS_KEY_LEGACY); |
| } catch (_) {} |
| } |
|
|
| function activeReport() { |
| return store.reports.find((r) => r.id === store.activeId) || null; |
| } |
|
|
| function activeDraftReport() { |
| const r = activeReport(); |
| return r && r.status !== 'saved' ? r : null; |
| } |
|
|
| function isCollectPage() { |
| const pg = val('curPage'); |
| return pg === 'explorar' || pg === 'arvore' || pg === 'sugestao'; |
| } |
|
|
| function showReportHeader() { |
| const pg = val('curPage'); |
| return pg === 'explorar' || pg === 'arvore' || pg === 'sugestao' || pg === 'relatorio'; |
| } |
|
|
| function draftReportsSorted() { |
| return [...store.reports.filter((r) => r.status !== 'saved')].sort((a, b) => { |
| const ta = new Date(a.updatedAt || a.createdAt || 0).getTime(); |
| const tb = new Date(b.updatedAt || b.createdAt || 0).getTime(); |
| return tb - ta; |
| }); |
| } |
|
|
| function resolveWorkActiveId() { |
| const drafts = draftReportsSorted(); |
| if (ui.lastWorkActiveId && drafts.some((r) => r.id === ui.lastWorkActiveId)) return ui.lastWorkActiveId; |
| if (store.activeId && drafts.some((r) => r.id === store.activeId)) return store.activeId; |
| return null; |
| } |
|
|
| function reportsForSidebar() { |
| let list; |
| if (ui.view === 'archive') list = store.reports.filter((r) => r.status === 'saved'); |
| else list = draftReportsSorted(); |
| const q = (ui.sidebarSearch || '').trim().toLowerCase(); |
| if (q) list = list.filter((r) => (r.name || '').toLowerCase().includes(q)); |
| return list; |
| } |
|
|
| function reportsForHeaderPicker() { |
| return draftReportsSorted(); |
| } |
|
|
| function scopeSnapshot() { |
| return { |
| sel: deps.selectionToParam(val('curSelection')), |
| cob: deps.filterParamFromSet(deps.cobSel), |
| sup: deps.filterParamFromSet(deps.supSel), |
| rop: deps.filterParamFromSet(deps.ropSel), |
| st: deps.filterParamFromSet(deps.stSel), |
| rupt: deps.ruptSel.has('pipe') ? 'pipe' : deps.ruptSel.has('liq') ? 'liq' : deps.ruptSel.has('1') ? '1' : '', |
| q: document.getElementById('busca')?.value?.trim() || '', |
| sort: val('sortK'), |
| asc: !!val('sortAsc'), |
| }; |
| } |
|
|
| function candidateRows() { |
| const out = []; |
| for (const x of deps.getFilteredList()) { |
| const s = deps.computeSugestao(x); |
| if (s) out.push({ x, s }); |
| } |
| return out; |
| } |
|
|
| function candidateCount() { |
| return candidateRows().length; |
| } |
|
|
| function findLive(p) { |
| const f = deps.findItem(p); |
| if (!f) return null; |
| const x = f.it; |
| return x._seg ? x : Object.assign({ _seg: f.seg }, x); |
| } |
|
|
| function projectedCob(x, orderQty) { |
| const dem = deps.demandM(x) || x.dem || 0; |
| if (dem <= 0) return null; |
| const onh = (x.o || 0) + (x.po_qp || 0) + (x.pr_q || 0) + (orderQty || 0); |
| return onh / dem; |
| } |
|
|
| function persistStore() { |
| storeRev += 1; |
| saveLocal(); |
| scheduleServerSave(); |
| } |
|
|
| function touchReport(r) { |
| r.updatedAt = new Date().toISOString(); |
| persistStore(); |
| } |
|
|
| function wireTitleInput(el, r) { |
| if (!el) return; |
| let nameTimer = null; |
| const applyName = (raw) => { |
| r.name = String(raw || '').trim() || r.name; |
| clearTimeout(nameTimer); |
| nameTimer = setTimeout(() => { |
| touchReport(r); |
| renderSidebar(); |
| renderHeaderBanner(); |
| }, 400); |
| }; |
| el.addEventListener('input', (e) => applyName(e.target.value)); |
| el.addEventListener('change', (e) => applyName(e.target.value)); |
| } |
|
|
| function newId() { |
| return 'r' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7); |
| } |
|
|
| function createReport(name, opts = {}) { |
| const enterCollect = opts.enterCollect !== false; |
| const r = { |
| id: newId(), |
| name: (name || '').trim() || defaultReportName(), |
| createdAt: new Date().toISOString(), |
| updatedAt: new Date().toISOString(), |
| status: 'draft', |
| scope: {}, |
| items: [], |
| loaded: false, |
| }; |
| store.reports.unshift(r); |
| store.activeId = r.id; |
| ui.lastWorkActiveId = r.id; |
| ui.archivePreviewId = null; |
| ui.view = 'work'; |
| persistStore(); |
| if (enterCollect) enterCollectMode(r.id); |
| deps.syncUrl(); |
| render(); |
| return r; |
| } |
|
|
| function performDeleteReport(id) { |
| store.reports = store.reports.filter((r) => r.id !== id); |
| if (ui.archivePreviewId === id) ui.archivePreviewId = null; |
| if (ui.lastWorkActiveId === id) ui.lastWorkActiveId = null; |
| if (store.activeId === id) { |
| store.activeId = ui.view === 'work' ? resolveWorkActiveId() : null; |
| if (ui.collecting) exitCollectMode(); |
| } |
| persistStore(); |
| deps.syncUrl(); |
| render(); |
| } |
|
|
| function openDeleteModal(id) { |
| const r = store.reports.find((x) => x.id === id); |
| if (!r) return; |
| deletePendingId = id; |
| const m = document.getElementById('rep-delete-modal'); |
| const msg = document.getElementById('rep-delete-msg'); |
| if (msg) msg.textContent = `Excluir relatório «${r.name}»? Esta ação não pode ser desfeita.`; |
| if (m) { |
| m.classList.add('open'); |
| m.setAttribute('aria-hidden', 'false'); |
| document.getElementById('rep-delete-cancel')?.focus(); |
| } |
| } |
|
|
| function closeDeleteModal() { |
| deletePendingId = null; |
| const m = document.getElementById('rep-delete-modal'); |
| if (!m) return; |
| m.classList.remove('open'); |
| m.setAttribute('aria-hidden', 'true'); |
| } |
|
|
| function confirmDeleteReport() { |
| const id = deletePendingId; |
| if (!id) return; |
| closeDeleteModal(); |
| performDeleteReport(id); |
| } |
|
|
| function deleteReport(id) { |
| openDeleteModal(id); |
| } |
|
|
| function archiveReport(id) { |
| const r = store.reports.find((x) => x.id === id); |
| if (!r || r.status === 'saved') return; |
| if (!r.items.length) { |
| alert('Adicione itens antes de arquivar.'); |
| return; |
| } |
| r.status = 'saved'; |
| r.savedAt = new Date().toISOString(); |
| touchReport(r); |
| if (store.activeId === id) { |
| if (ui.lastWorkActiveId === id) ui.lastWorkActiveId = null; |
| store.activeId = null; |
| if (ui.collecting) exitCollectMode(); |
| } |
| ui.view = 'archive'; |
| ui.archivePreviewId = id; |
| persistStore(); |
| deps.syncUrl(); |
| render(); |
| } |
|
|
| function restoreReport(id) { |
| const r = store.reports.find((x) => x.id === id); |
| if (!r || r.status !== 'saved') return; |
| r.status = 'draft'; |
| delete r.savedAt; |
| touchReport(r); |
| store.activeId = id; |
| ui.lastWorkActiveId = id; |
| ui.archivePreviewId = null; |
| ui.view = 'work'; |
| persistStore(); |
| deps.syncUrl(); |
| render(); |
| } |
|
|
| function selectReport(id) { |
| const r = store.reports.find((x) => x.id === id); |
| if (!r) return; |
| if (ui.view === 'archive' || r.status === 'saved') { |
| ui.archivePreviewId = id; |
| render(); |
| return; |
| } |
| store.activeId = id; |
| ui.lastWorkActiveId = id; |
| ui.archivePreviewId = null; |
| persistStore(); |
| deps.syncUrl?.(); |
| if (isCollectPage()) { |
| enterCollectMode(id); |
| return; |
| } |
| renderHeaderBanner(); |
| refreshPickCells(); |
| render(); |
| } |
|
|
| function setView(view) { |
| ui.view = view === 'archive' ? 'archive' : 'work'; |
| if (ui.view === 'archive') { |
| const cur = activeReport(); |
| if (cur && cur.status !== 'saved') ui.lastWorkActiveId = store.activeId; |
| store.activeId = null; |
| if (ui.collecting) exitCollectMode(); |
| if ( |
| ui.archivePreviewId && |
| !store.reports.some((x) => x.id === ui.archivePreviewId && x.status === 'saved') |
| ) { |
| ui.archivePreviewId = null; |
| } |
| } else { |
| store.activeId = resolveWorkActiveId(); |
| ui.archivePreviewId = null; |
| } |
| persistStore(); |
| deps.syncUrl(); |
| render(); |
| } |
|
|
| function getView() { |
| return ui.view; |
| } |
|
|
| function loadSuggestions() { |
| const r = activeReport(); |
| if (!r) return; |
| const rows = candidateRows(); |
| const existing = new Set(r.items.map((i) => i.p)); |
| for (const { x, s } of rows) { |
| if (existing.has(x.p)) continue; |
| r.items.push({ |
| p: x.p, |
| seg: x._seg || x.seg || '', |
| qty: s.qty, |
| qtySug: s.qty, |
| decision: 'pending', |
| note: '', |
| }); |
| existing.add(x.p); |
| } |
| r.loaded = true; |
| r.scope = scopeSnapshot(); |
| touchReport(r); |
| render(); |
| } |
|
|
| function reportRowEntries(r) { |
| const out = []; |
| for (const ri of r.items || []) { |
| const x = findLive(ri.p); |
| if (!x) continue; |
| const s = deps.computeSugestao(x) || null; |
| out.push({ x, s, ri }); |
| } |
| return out; |
| } |
|
|
| function isCollecting() { |
| return !!ui.collecting; |
| } |
|
|
| function getActiveReport() { |
| return activeReport(); |
| } |
|
|
| function isInReport(part) { |
| const r = activeReport(); |
| if (!r) return false; |
| const code = String(part || '').trim(); |
| return (r.items || []).some((i) => i.p === code); |
| } |
|
|
| function enterCollectMode(reportId) { |
| if (reportId) { |
| store.activeId = reportId; |
| ui.lastWorkActiveId = reportId; |
| } |
| ui.collecting = true; |
| persistStore(); |
| renderHeaderBanner(); |
| renderSidebar(); |
| refreshPickCells(); |
| deps.syncUrl?.(); |
| if (val('curPage') === 'relatorio') goPage('explorar'); |
| } |
|
|
| function exitCollectMode() { |
| ui.collecting = false; |
| renderHeaderBanner(); |
| renderSidebar(); |
| refreshPickCells(); |
| } |
|
|
| let headerReportMenuOpen = false; |
|
|
| function setHeaderReportMenuOpen(open) { |
| headerReportMenuOpen = !!open; |
| const btn = document.getElementById('header-report-name-btn'); |
| const menu = document.getElementById('header-report-menu'); |
| if (btn) btn.setAttribute('aria-expanded', headerReportMenuOpen ? 'true' : 'false'); |
| if (menu) menu.hidden = !headerReportMenuOpen; |
| } |
|
|
| function renderHeaderReportMenu() { |
| const menu = document.getElementById('header-report-menu'); |
| if (!menu) return; |
| const list = reportsForHeaderPicker(); |
| if (!list.length) { |
| menu.innerHTML = '<div class="header-report-menu-empty">Nenhum rascunho em andamento.</div>'; |
| return; |
| } |
| menu.innerHTML = list |
| .map((rep) => { |
| const on = rep.id === store.activeId ? ' on' : ''; |
| const { secondary } = sidebarCardMeta(rep); |
| return `<button type="button" class="header-report-menu-item${on}" role="option" aria-selected="${rep.id === store.activeId ? 'true' : 'false'}" data-header-rep-id="${attrEsc(rep.id)}"> |
| <span class="nm">${esc(rep.name)}</span> |
| <span class="meta">${esc(secondary)}</span> |
| </button>`; |
| }) |
| .join(''); |
| menu.querySelectorAll('[data-header-rep-id]').forEach((btn) => { |
| btn.onclick = () => { |
| selectReport(btn.dataset.headerRepId); |
| setHeaderReportMenuOpen(false); |
| }; |
| }); |
| } |
|
|
| function renderHeaderBanner() { |
| const wrap = document.getElementById('header-report-mode'); |
| const nameEl = document.getElementById('header-report-name'); |
| const nameBtn = document.getElementById('header-report-name-btn'); |
| const metaEl = document.getElementById('header-report-meta'); |
| const exitBtn = document.getElementById('header-report-exit'); |
| const pill = document.getElementById('header-report-pill'); |
| if (!wrap) return; |
|
|
| if (!showReportHeader()) { |
| wrap.hidden = true; |
| setHeaderReportMenuOpen(false); |
| return; |
| } |
|
|
| wrap.hidden = false; |
| const draft = activeDraftReport(); |
| const empty = !draft; |
|
|
| if (nameBtn) nameBtn.classList.toggle('is-empty', empty); |
| if (pill) pill.classList.toggle('is-collecting', !!ui.collecting); |
|
|
| if (empty) { |
| if (nameEl) nameEl.textContent = 'Nenhum selecionado'; |
| if (metaEl) { |
| metaEl.textContent = ''; |
| metaEl.hidden = true; |
| } |
| if (exitBtn) exitBtn.hidden = true; |
| } else { |
| if (nameEl) nameEl.textContent = draft.name || '—'; |
| const n = reportItemCount(draft); |
| if (metaEl) { |
| metaEl.textContent = n ? `${n} itens` : 'Vazio'; |
| metaEl.hidden = false; |
| } |
| if (exitBtn) { |
| exitBtn.hidden = false; |
| if (ui.collecting) { |
| exitBtn.textContent = 'Sair do modo'; |
| exitBtn.title = 'Parar de adicionar itens'; |
| } else { |
| exitBtn.textContent = 'Coletar itens'; |
| exitBtn.title = 'Ativar modo coleta neste relatório'; |
| } |
| } |
| } |
|
|
| renderHeaderReportMenu(); |
| } |
|
|
| function refreshPickCells(root) { |
| const scope = root || document; |
| const draft = activeDraftReport(); |
| scope.querySelectorAll('tr[data-p]').forEach((tr) => { |
| const p = tr.getAttribute('data-p'); |
| if (!p) return; |
| tr.classList.toggle('rep-in-report', !!draft && isInReport(p)); |
| const cell = tr.querySelector('.rep-pick-cell'); |
| if (!cell) return; |
| const seg = tr.getAttribute('data-seg') || ''; |
| cell.innerHTML = pickButtonInner(p, seg); |
| }); |
| } |
|
|
| function pickButtonInner(p, seg) { |
| if (!isCollectPage()) return ''; |
| const pe = attrEsc(p); |
| const se = attrEsc(seg); |
| if (activeDraftReport() && isInReport(p)) { |
| return `<button type="button" class="rep-added-btn" data-rep-pick="add" data-p="${pe}" data-seg="${se}" title="No relatório — clique para remover">✓</button>`; |
| } |
| return `<button type="button" class="row-rep-act row-rep-act-always" data-rep-pick="add" data-p="${pe}" data-seg="${se}" title="Adicionar ao relatório">+</button>`; |
| } |
|
|
| function pickCellHtml(p, seg) { |
| return `<td class="rep-pick-cell">${pickButtonInner(p, seg)}</td>`; |
| } |
|
|
| function defaultReportName() { |
| return ( |
| 'Relatório · ' + |
| new Date().toLocaleString('pt-BR', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) |
| ); |
| } |
|
|
| function addItem(part, seg) { |
| const r = activeReport(); |
| if (!r) return false; |
| const code = String(part || '').trim(); |
| if (!code || isInReport(code)) return false; |
| const live = findLive(code); |
| const x = live || { p: code, _seg: seg || '' }; |
| const s = deps.computeSugestao(x); |
| const qty = s ? s.qty : 0; |
| r.items.push({ |
| p: code, |
| seg: seg || itemSeg(x) || '', |
| qty, |
| qtySug: qty, |
| decision: 'pending', |
| note: '', |
| }); |
| r.loaded = true; |
| touchReport(r); |
| renderHeaderBanner(); |
| refreshPickCells(); |
| if (val('curPage') === 'relatorio') render(); |
| else { |
| renderSidebar(); |
| renderTableOnly(); |
| } |
| return true; |
| } |
|
|
| function removeItem(part) { |
| const r = activeReport(); |
| if (!r) return false; |
| const code = String(part || '').trim(); |
| const before = r.items.length; |
| r.items = (r.items || []).filter((i) => i.p !== code); |
| if (r.items.length === before) return false; |
| touchReport(r); |
| refreshPickCells(); |
| if (val('curPage') === 'relatorio') render(); |
| else { |
| renderSidebar(); |
| renderTableOnly(); |
| } |
| return true; |
| } |
|
|
| function draftsForPick() { |
| return store.reports |
| .filter((r) => r.status !== 'saved') |
| .sort((a, b) => new Date(b.updatedAt || b.createdAt) - new Date(a.updatedAt || a.createdAt)); |
| } |
|
|
| function openPickModal(onPick) { |
| pickPending = onPick || null; |
| const m = document.getElementById('rep-pick-modal'); |
| const list = document.getElementById('rep-pick-list'); |
| if (!m || !list) { |
| openNewModal(true, pickPending); |
| return; |
| } |
| const drafts = draftsForPick(); |
| if (!drafts.length) { |
| openNewModal(true, pickPending); |
| return; |
| } |
| list.innerHTML = drafts |
| .map((r) => { |
| const n = (r.items || []).length; |
| return `<button type="button" class="rep-pick-item" data-rep-pick-id="${attrEsc(r.id)}"> |
| <span class="nm">${esc(r.name)}</span> |
| <span class="meta">${n} itens · ${esc(new Date(r.updatedAt || r.createdAt).toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' }))}</span> |
| </button>`; |
| }) |
| .join(''); |
| list.querySelectorAll('[data-rep-pick-id]').forEach((btn) => { |
| btn.onclick = () => { |
| const fn = pickPending; |
| pickPending = null; |
| selectReport(btn.dataset.repPickId); |
| closePickModal(); |
| if (fn) fn(); |
| }; |
| }); |
| m.classList.add('open'); |
| m.setAttribute('aria-hidden', 'false'); |
| } |
|
|
| function closePickModal() { |
| const m = document.getElementById('rep-pick-modal'); |
| if (!m) return; |
| m.classList.remove('open'); |
| m.setAttribute('aria-hidden', 'true'); |
| } |
|
|
| function cancelPickPending() { |
| pickPending = null; |
| closePickModal(); |
| openNewModal(false); |
| } |
|
|
| function promptPickOrCreateReport(thenAdd) { |
| openPickModal(thenAdd); |
| } |
|
|
| function toggleItem(part, seg) { |
| const code = String(part || '').trim(); |
| if (!code) return; |
| if (isInReport(code)) { |
| removeItem(code); |
| return; |
| } |
| const doAdd = () => addItem(code, seg); |
| if (!ui.collecting || !activeReport()) { |
| promptPickOrCreateReport(doAdd); |
| return; |
| } |
| doAdd(); |
| } |
|
|
| function itemSeg(x) { |
| return x._seg || x.seg || ''; |
| } |
|
|
| function matchesSegFilter(entry) { |
| const f = ui.segFilter; |
| if (!f || f === 'all') return true; |
| const { x, s, ri } = entry; |
| if (f === 'prime') return !!(s && s.primeRep); |
| if (f === 'rop') { |
| return deps.itemUsesRop(x) && ((x.rop || 0) > 0 || (ri.qtySug || 0) > 0 || (ri.qty || 0) > 0); |
| } |
| if (f === 'po') return supplyGroupKey(x) === 'po'; |
| if (f === 'pr') return supplyGroupKey(x) === 'pr'; |
| return itemSeg(x) === f; |
| } |
|
|
| function filterTableRows(rows) { |
| let list = rows.filter(matchesSegFilter); |
| const q = ui.tableSearch.trim().toLowerCase(); |
| if (!q) return list; |
| return list.filter( |
| (o) => |
| o.x.p.toLowerCase().includes(q) || |
| (o.x.d || '').toLowerCase().includes(q) || |
| (o.x.fam || '').toLowerCase().includes(q) |
| ); |
| } |
|
|
| function reportSortCobValue(x) { |
| const m = deps.cobMetrics?.(x); |
| if (!m) return null; |
| if (ui.tableSortAsc && m.pct > 1) return 1 + m.pct; |
| return m.pct; |
| } |
|
|
| function reportSortValue(entry) { |
| const { x, s, ri } = entry; |
| const k = ui.tableSortK; |
| const r = activeReport(); |
| if (!k || k === 'ord') { |
| const idx = (r?.items || []).findIndex((i) => i.p === x.p); |
| return idx < 0 ? Infinity : idx; |
| } |
| if (k === '_seg') return itemSeg(x); |
| if (k === 'p') return x.p; |
| if (k === 'd') return x.d || ''; |
| if (k === 'fam') return x.fam || ''; |
| if (k === 'o') return x.o; |
| if (k === 'cob') return reportSortCobValue(x); |
| if (k === 'qty') return ri.qty; |
| if (k === 'proj') return projectedCob(x, ri.qty); |
| if (k === 'alvo') return s?.alvoRef ?? null; |
| return x.p; |
| } |
|
|
| function sortReportRows(rows) { |
| if (!ui.tableSortK) return rows; |
| const asc = ui.tableSortAsc; |
| const nullV = asc ? Infinity : -Infinity; |
| return [...rows].sort((a, b) => { |
| const av = reportSortValue(a); |
| const bv = reportSortValue(b); |
| const aN = av == null ? nullV : av; |
| const bN = bv == null ? nullV : bv; |
| if (typeof aN === 'string' || typeof bN === 'string') { |
| return asc ? ('' + aN).localeCompare(bN, 'pt-BR') : ('' + bN).localeCompare(aN, 'pt-BR'); |
| } |
| if (aN !== bN) return asc ? aN - bN : bN - aN; |
| return (a.x.p || '').localeCompare(b.x.p || '', 'pt-BR'); |
| }); |
| } |
|
|
| function defaultRepSortAsc(k) { |
| return k === 'ord' || k === 'p' || k === 'd' || k === 'fam' || k === '_seg' || k === 'cob'; |
| } |
|
|
| function updateRepSortHeaders() { |
| document.querySelectorAll('#rep-app .rep-table-single thead th[data-rep-k]').forEach((th) => { |
| const on = th.dataset.repK === ui.tableSortK; |
| th.classList.toggle('on', on); |
| th.setAttribute('aria-sort', on ? (ui.tableSortAsc ? 'ascending' : 'descending') : 'none'); |
| if (on) th.dataset.dir = ui.tableSortAsc ? 'asc' : 'desc'; |
| else delete th.dataset.dir; |
| }); |
| } |
|
|
| function countForFilter(rows, filterKey) { |
| const prev = ui.segFilter; |
| ui.segFilter = filterKey; |
| const n = rows.filter((e) => matchesSegFilter(e)).length; |
| ui.segFilter = prev; |
| return n; |
| } |
|
|
| function reportItemCount(r) { |
| return (r.items || []).length; |
| } |
|
|
| function reportValueRs(r) { |
| let total = 0; |
| for (const ri of r.items || []) { |
| const x = findLive(ri.p); |
| if (!x) continue; |
| total += (ri.qty || 0) * (x.uc || 0); |
| } |
| return total; |
| } |
|
|
| function reportSummaryText(r) { |
| const n = reportItemCount(r); |
| const val = reportValueRs(r); |
| const valTxt = val > 0 && deps.fmtBRL ? ' · ' + deps.fmtBRL(val) : ''; |
| return `${n.toLocaleString('pt-BR')} itens${valTxt}`; |
| } |
|
|
| function formatRelativeTime(iso) { |
| if (!iso) return ''; |
| const then = new Date(iso).getTime(); |
| if (Number.isNaN(then)) return ''; |
| const diff = Date.now() - then; |
| const mins = Math.floor(diff / 60000); |
| if (mins < 1) return 'agora'; |
| if (mins < 60) return `há ${mins} min`; |
| const hrs = Math.floor(mins / 60); |
| if (hrs < 24) return `há ${hrs}h`; |
| const days = Math.floor(hrs / 24); |
| if (days < 7) return `há ${days}d`; |
| return new Date(iso).toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' }); |
| } |
|
|
| function sidebarCardMeta(r) { |
| const n = reportItemCount(r); |
| const val = reportValueRs(r); |
| const valTxt = val > 0 && deps.fmtBRL ? deps.fmtBRL(val) : ''; |
| const countTxt = n === 0 ? 'Aguardando itens' : `${n.toLocaleString('pt-BR')} itens`; |
| const secondary = valTxt ? `${countTxt} · ${valTxt}` : countTxt; |
| const when = formatRelativeTime(r.updatedAt || r.createdAt); |
| return { secondary, when, empty: n === 0 }; |
| } |
|
|
| function goPage(page) { |
| const fn = global.navPage; |
| if (typeof fn === 'function') fn(page); |
| } |
|
|
| function wireEmptyCtas(el, r) { |
| el.querySelector('#rep-cta-collect')?.addEventListener('click', () => { |
| enterCollectMode(r.id); |
| goPage('explorar'); |
| }); |
| el.querySelectorAll('[data-rep-cta-page]').forEach((btn) => { |
| btn.addEventListener('click', () => goPage(btn.dataset.repCtaPage)); |
| }); |
| } |
|
|
| function alvoText(s) { |
| return s.modo === 'rop' ? 'ROP ' + deps.fmtN(s.alvoRef) : deps.fmtAlvo(s.alvoRef) + ' m'; |
| } |
|
|
| function projText(x, qty) { |
| const proj = projectedCob(x, qty); |
| return proj == null ? '—' : proj.toFixed(1) + ' m'; |
| } |
|
|
| function supplyGroupKey(x) { |
| const s = x.sup || 'none'; |
| if (s === 'po') return 'po'; |
| if (s === 'pr') return 'pr'; |
| return 'none'; |
| } |
|
|
| function repPreviewRowHtml({ x, s, ri }) { |
| const cobMain = deps.cobMainText(x); |
| const bar = deps.cobBar ? deps.cobBar(x) : ''; |
| const saldo = (x.o || 0) <= 0 ? '<span class="zero">0</span>' : deps.fmtN(x.o); |
| const qtyTxt = `${deps.fmtN ? deps.fmtN(ri.qty) : ri.qty} ${esc(x.u || '')}`.trim(); |
| const seg = itemSeg(x); |
| const fam = (x.fam || '').trim(); |
| const badges = deps.itemBadgesHtml ? deps.itemBadgesHtml(x) : ''; |
| const badgeHtml = badges ? `<span class="desc-badges">${badges}</span>` : ''; |
| return `<tr data-rep-part="${attrEsc(x.p)}" class="rep-data-row rep-preview-row"> |
| <td class="seg"><span class="pill-seg">${esc(seg)}</span></td> |
| <td class="l code">${esc(x.p)}</td> |
| <td class="l desc" title="${esc(x.d || '')}"><div class="desc-cell">${badgeHtml}<span class="desc-txt">${esc(x.d || '')}</span></div></td> |
| <td class="l rep-col-fam" title="${esc(fam)}"><span class="desc-txt">${esc(fam || '—')}</span></td> |
| <td class="saldo-qty">${saldo}</td> |
| <td><span class="cob-cell">${bar}<span class="cob-vals"><span class="cob-main">${cobMain}</span></span></span></td> |
| <td class="rep-qty-cell rep-qty-read">${esc(qtyTxt)}</td> |
| <td class="rep-proj">${projText(x, ri.qty)}</td> |
| <td class="rep-alvo">${s ? alvoText(s) : '—'}</td> |
| </tr>`; |
| } |
|
|
| function previewTableColgroup() { |
| return `<colgroup> |
| <col style="width:34px"> |
| <col style="width:76px"> |
| <col> |
| <col style="width:14%"> |
| <col style="width:48px"> |
| <col style="width:72px"> |
| <col style="width:64px"> |
| <col style="width:58px"> |
| <col style="width:52px"> |
| </colgroup>`; |
| } |
|
|
| function previewTableHead() { |
| return `${previewTableColgroup()}<thead><tr> |
| <th class="seg">Seg</th><th>Código</th><th>Descrição</th><th>Família</th><th>Saldo</th><th>Cob.</th><th>Qty</th><th>Cob.+ped.</th><th>Alvo</th> |
| </tr></thead>`; |
| } |
|
|
| function repRowHtml({ x, s, ri }) { |
| const cobMain = deps.cobMainText(x); |
| const bar = deps.cobBar ? deps.cobBar(x) : ''; |
| const saldo = (x.o || 0) <= 0 ? '<span class="zero">0</span>' : deps.fmtN(x.o); |
| const qtyCell = `<input type="number" class="rep-qty-in" data-rep-part="${attrEsc(x.p)}" min="0" step="any" value="${ri.qty}"> <span class="rep-uom-hint">${esc(x.u || '')}</span>`; |
| const actions = `<td class="rep-actions"> |
| <button type="button" class="rep-act-remove" data-rep-act="remove" data-rep-part="${attrEsc(x.p)}" title="Remover do relatório">−</button> |
| </td>`; |
| const seg = itemSeg(x); |
| const fam = (x.fam || '').trim(); |
| const badges = deps.itemBadgesHtml ? deps.itemBadgesHtml(x) : ''; |
| const badgeHtml = badges ? `<span class="desc-badges">${badges}</span>` : ''; |
| return `<tr data-rep-part="${attrEsc(x.p)}" class="rep-data-row"> |
| ${actions} |
| <td class="seg"><span class="pill-seg">${esc(seg)}</span></td> |
| <td class="l code">${esc(x.p)}</td> |
| <td class="l desc" title="${esc(x.d || '')}"><div class="desc-cell">${badgeHtml}<span class="desc-txt">${esc(x.d || '')}</span></div></td> |
| <td class="l rep-col-fam" title="${esc(fam)}"><span class="desc-txt">${esc(fam || '—')}</span></td> |
| <td class="saldo-qty">${saldo}</td> |
| <td><span class="cob-cell">${bar}<span class="cob-vals"><span class="cob-main">${cobMain}</span></span></span></td> |
| <td class="rep-qty-cell">${qtyCell}</td> |
| <td class="rep-proj" data-proj-for="${attrEsc(x.p)}">${projText(x, ri.qty)}</td> |
| <td class="rep-alvo">${s ? alvoText(s) : '—'}</td> |
| </tr>`; |
| } |
|
|
| function tableColgroup() { |
| return `<colgroup> |
| <col class="col-rep-actions" style="width:32px"> |
| <col style="width:34px"> |
| <col style="width:76px"> |
| <col> |
| <col style="width:14%"> |
| <col style="width:48px"> |
| <col style="width:72px"> |
| <col style="width:64px"> |
| <col style="width:58px"> |
| <col style="width:52px"> |
| </colgroup>`; |
| } |
|
|
| function repTh(k, label, cls = '', title = '') { |
| const t = title ? ` title="${attrEsc(title)}"` : ''; |
| const c = cls ? ` class="${cls}"` : ''; |
| return `<th${c} data-rep-k="${attrEsc(k)}"${t}>${label}</th>`; |
| } |
|
|
| function tableHead() { |
| return `${tableColgroup()}<thead><tr> |
| ${repTh('ord', '', 'rep-actions-hd', 'Ordem no relatório')} |
| ${repTh('_seg', 'Seg', 'seg')} |
| ${repTh('p', 'Código', 'l')} |
| ${repTh('d', 'Descrição', 'l')} |
| ${repTh('fam', 'Família', 'l')} |
| ${repTh('o', 'Saldo')} |
| ${repTh('cob', 'Cob.', '', 'Cobertura vs alvo')} |
| ${repTh('qty', 'Qty')} |
| ${repTh('proj', 'Cob.+ped.', '', 'Cobertura projetada com pedido')} |
| ${repTh('alvo', 'Alvo')} |
| </tr></thead>`; |
| } |
|
|
| function setSegFilter(f) { |
| const segs = deps.SEGS || []; |
| const nkeys = deps.NKEYS || []; |
| const extra = ['rop', 'po', 'pr', 'prime', ...nkeys]; |
| let next = f; |
| if (next !== 'all' && !extra.includes(next) && !segs.includes(next)) next = 'all'; |
| ui.segFilter = next; |
| saveLocal(); |
| deps.syncUrl(); |
| renderSegNav(); |
| renderTableOnly(); |
| } |
|
|
| function getSegFilter() { |
| return ui.segFilter || 'all'; |
| } |
|
|
| function pushSegSep(pills) { |
| if (pills.length) pills.push('<span class="rep-seg-sep" aria-hidden="true"></span>'); |
| } |
|
|
| function renderSegNav() { |
| const nav = document.getElementById('rep-seg-nav'); |
| const r = activeReport(); |
| if (!nav || !r || !r.items.length) return; |
| const entries = reportRowEntries(r); |
| const segs = deps.SEGS || []; |
| const pills = []; |
| const allN = countForFilter(entries, 'all'); |
| pills.push( |
| `<button type="button" class="rep-seg-pill${ui.segFilter === 'all' ? ' on' : ''}" data-rep-seg="all">Todos<span class="rep-seg-n">(${allN})</span></button>` |
| ); |
| let lastAbc = ''; |
| let abcHadPill = false; |
| for (const seg of segs) { |
| const n = countForFilter(entries, seg); |
| if (n <= 0 && ui.segFilter !== seg) continue; |
| const abc = seg[0]; |
| if (abc !== lastAbc) { |
| if (lastAbc && abcHadPill) pushSegSep(pills); |
| lastAbc = abc; |
| abcHadPill = false; |
| } |
| pills.push( |
| `<button type="button" class="rep-seg-pill${ui.segFilter === seg ? ' on' : ''}" data-rep-seg="${esc(seg)}">${esc(seg)}<span class="rep-seg-n">(${n})</span></button>` |
| ); |
| abcHadPill = true; |
| } |
| const poN = countForFilter(entries, 'po'); |
| if (poN > 0 || ui.segFilter === 'po') { |
| pushSegSep(pills); |
| pills.push( |
| `<button type="button" class="rep-seg-pill rep-seg-po${ui.segFilter === 'po' ? ' on' : ''}" data-rep-seg="po" title="Com ordem de compra aberta">OC<span class="rep-seg-n">(${poN})</span></button>` |
| ); |
| } |
| const prN = countForFilter(entries, 'pr'); |
| if (prN > 0 || ui.segFilter === 'pr') { |
| pills.push( |
| `<button type="button" class="rep-seg-pill rep-seg-pr${ui.segFilter === 'pr' ? ' on' : ''}" data-rep-seg="pr" title="Com requisição aberta">PR<span class="rep-seg-n">(${prN})</span></button>` |
| ); |
| } |
| const ni0N = countForFilter(entries, 'NI0'); |
| const primeN = countForFilter(entries, 'prime'); |
| if (ni0N > 0 || ui.segFilter === 'NI0' || entries.some((e) => itemSeg(e.x) === 'NI0')) { |
| pushSegSep(pills); |
| pills.push( |
| `<button type="button" class="rep-seg-pill rep-seg-ni0${ui.segFilter === 'NI0' ? ' on' : ''}" data-rep-seg="NI0" title="Garantia s/ saldo — 1ª reposição">NI0<span class="rep-seg-n">(${ni0N})</span></button>` |
| ); |
| if (primeN > 0 || ui.segFilter === 'prime') { |
| pills.push( |
| `<button type="button" class="rep-seg-pill rep-seg-prime${ui.segFilter === 'prime' ? ' on' : ''}" data-rep-seg="prime" title="NI0 / NF0 novo (≤6m) com sugestão automática">1ª rep.<span class="rep-seg-n">(${primeN})</span></button>` |
| ); |
| } |
| } |
| const ropN = countForFilter(entries, 'rop'); |
| if (ropN > 0 || ui.segFilter === 'rop') { |
| pushSegSep(pills); |
| pills.push( |
| `<button type="button" class="rep-seg-pill rep-seg-rop${ui.segFilter === 'rop' ? ' on' : ''}" data-rep-seg="rop">Sem giro (ROP)<span class="rep-seg-n">(${ropN})</span></button>` |
| ); |
| } |
| nav.setAttribute('aria-label', 'Segmento e pedidos abertos'); |
| nav.innerHTML = pills.join(''); |
| nav.querySelectorAll('[data-rep-seg]').forEach((btn) => { |
| btn.onclick = () => setSegFilter(btn.dataset.repSeg); |
| }); |
| } |
|
|
| function updateQty(p, qty) { |
| const r = activeReport(); |
| if (!r) return; |
| const ri = r.items.find((i) => i.p === p); |
| if (!ri) return; |
| ri.qty = Number.isFinite(qty) ? qty : 0; |
| touchReport(r); |
| const entry = reportRowEntries(r).find((e) => e.ri.p === p); |
| if (!entry) return; |
| document.querySelectorAll(`[data-proj-for="${CSS.escape(p)}"]`).forEach((el) => { |
| el.textContent = projText(entry.x, ri.qty); |
| }); |
| } |
|
|
| function preserveLocalDecisions(serverReports) { |
| const byId = new Map(store.reports.map((r) => [r.id, r])); |
| const serverIds = new Set((serverReports || []).map((r) => r.id)); |
| const out = (serverReports || []).map((sr) => { |
| const local = byId.get(sr.id); |
| if (!local) return migrateReport({ ...sr }); |
| const localItems = new Map(local.items.map((i) => [i.p, i])); |
| const mergedItems = (sr.items || []).map((it) => { |
| const li = localItems.get(it.p); |
| if (!li) return migrateItem({ ...it }); |
| return migrateItem({ |
| ...it, |
| decision: li.decision || it.decision, |
| qty: li.qty ?? it.qty, |
| qtySug: li.qtySug ?? it.qtySug, |
| note: li.note != null ? li.note : it.note, |
| approvedAt: li.approvedAt || it.approvedAt, |
| rejectedAt: li.rejectedAt || it.rejectedAt, |
| }); |
| }); |
| const serverCodes = new Set((sr.items || []).map((i) => i.p)); |
| for (const li of local.items || []) { |
| if (!serverCodes.has(li.p)) mergedItems.push(migrateItem({ ...li })); |
| } |
| return migrateReport({ ...sr, items: mergedItems }); |
| }); |
| for (const local of store.reports) { |
| if (!serverIds.has(local.id)) out.push(migrateReport({ ...local })); |
| } |
| return out; |
| } |
|
|
| function applyServerStore(serverReports, revAtSend) { |
| if (storeRev > revAtSend) return false; |
| store.reports = preserveLocalDecisions(serverReports); |
| migrateStore(); |
| return true; |
| } |
|
|
| function updateSaveBadge() { |
| const badge = document.getElementById('rep-save-badge'); |
| if (!badge) return; |
| if (!ui.serverAvailable) { |
| badge.textContent = 'Autosave · local'; |
| return; |
| } |
| if (saveInFlight) badge.textContent = 'Salvando…'; |
| else if (saveQueued || saveStatus === 'pending') badge.textContent = 'Pendente'; |
| else if (saveStatus === 'saved') badge.textContent = 'Salvo'; |
| else badge.textContent = 'Autosave · servidor'; |
| } |
|
|
| function setSaveStatus(status) { |
| saveStatus = status; |
| updateSaveBadge(); |
| } |
|
|
| function wireRepTableSort() { |
| const root = document.getElementById('rep-app'); |
| if (!root || root._repSortWired) return; |
| root._repSortWired = true; |
| root.addEventListener('click', (e) => { |
| const th = e.target.closest('.rep-table-single thead th[data-rep-k]'); |
| if (!th || !root.contains(th)) return; |
| const k = th.dataset.repK; |
| if (!k) return; |
| if (ui.tableSortK === k) ui.tableSortAsc = !ui.tableSortAsc; |
| else { |
| ui.tableSortK = k; |
| ui.tableSortAsc = defaultRepSortAsc(k); |
| } |
| renderTableOnly(); |
| }); |
| } |
|
|
| function wireRepTableClicks() { |
| const root = document.getElementById('rep-app'); |
| if (!root || root._repClickWired) return; |
| root._repClickWired = true; |
| root.addEventListener( |
| 'click', |
| (ev) => { |
| const btn = ev.target.closest('button[data-rep-act]'); |
| if (!btn || !root.contains(btn)) return; |
| ev.preventDefault(); |
| ev.stopPropagation(); |
| const act = btn.getAttribute('data-rep-act'); |
| const p = btn.getAttribute('data-rep-part'); |
| if (!p || !act) return; |
| if (act === 'remove') removeItem(p); |
| }, |
| true |
| ); |
| root.addEventListener('change', (ev) => { |
| const inp = ev.target.closest('.rep-qty-in'); |
| if (!inp || !root.contains(inp)) return; |
| const p = inp.getAttribute('data-rep-part'); |
| if (p) updateQty(p, parseFloat(inp.value)); |
| }); |
| root.addEventListener('click', (ev) => { |
| if (ev.target.closest('button[data-rep-act], .rep-qty-in, input, textarea, label')) return; |
| const row = ev.target.closest('tr.rep-data-row'); |
| if (!row || !root.contains(row)) return; |
| const p = row.getAttribute('data-rep-part'); |
| if (!p || !deps.openItemInModal) return; |
| document.querySelectorAll('#rep-app tr.rep-data-row').forEach((t) => { |
| t.classList.toggle('sel', t.getAttribute('data-rep-part') === p); |
| }); |
| deps.openItemInModal(null, p); |
| }); |
| } |
|
|
| function renderTableOnly() { |
| const r = activeReport(); |
| const body = document.getElementById('rep-tbody'); |
| if (!r || !body) return; |
|
|
| const rows = sortReportRows(filterTableRows(reportRowEntries(r))); |
| const segLblMap = { rop: ' (sem giro ROP)', po: ' (com OC)', pr: ' (com PR)', prime: ' (1ª rep.)' }; |
| const segLbl = |
| ui.segFilter === 'all' || !ui.segFilter |
| ? '' |
| : segLblMap[ui.segFilter] || ` (${ui.segFilter})`; |
| body.innerHTML = rows.length |
| ? rows.map((e) => repRowHtml(e)).join('') |
| : `<tr><td colspan="${COLS}" class="rep-empty-row">Nenhum item${esc(segLbl)} neste filtro.</td></tr>`; |
|
|
| const prog = document.getElementById('rep-counts'); |
| if (prog) prog.textContent = reportSummaryText(r); |
| renderSegNav(); |
| updateRepSortHeaders(); |
| } |
|
|
| function openNewModal(show, afterCreate) { |
| if (show && afterCreate) pickPending = afterCreate; |
| const m = document.getElementById('rep-new-modal'); |
| if (!m) return; |
| m.classList.toggle('open', !!show); |
| m.setAttribute('aria-hidden', show ? 'false' : 'true'); |
| if (show) { |
| const inp = document.getElementById('rep-new-name'); |
| if (inp) { |
| inp.value = ''; |
| inp.placeholder = 'Ex.: CZ ruptura jun/26'; |
| inp.focus(); |
| } |
| } |
| } |
|
|
| function renderSidebar() { |
| const ul = document.getElementById('rep-report-list'); |
| const searchInp = document.getElementById('rep-sidebar-search'); |
| if (searchInp && searchInp.value !== ui.sidebarSearch) searchInp.value = ui.sidebarSearch; |
| if (!ul) return; |
| const allList = |
| ui.view === 'archive' |
| ? store.reports.filter((r) => r.status === 'saved') |
| : store.reports.filter((r) => r.status !== 'saved'); |
| const list = reportsForSidebar(); |
| if (!list.length) { |
| const filtered = !!(ui.sidebarSearch || '').trim(); |
| ul.innerHTML = |
| ui.view === 'archive' |
| ? filtered |
| ? '<li class="rep-empty-sidebar"><span class="rep-empty-sidebar-icon" aria-hidden="true">⌕</span>Nenhum relatório salvo corresponde ao filtro.</li>' |
| : '<li class="rep-empty-sidebar"><span class="rep-empty-sidebar-icon" aria-hidden="true">📁</span>Nenhum relatório salvo ainda.</li>' |
| : allList.length && filtered |
| ? '<li class="rep-empty-sidebar"><span class="rep-empty-sidebar-icon" aria-hidden="true">⌕</span>Nenhum rascunho corresponde ao filtro.</li>' |
| : '<li class="rep-empty-sidebar"><span class="rep-empty-sidebar-icon" aria-hidden="true">+</span>Nenhum rascunho ativo.<br>Veja <strong>Arquivados</strong> ou use <strong>+ Novo</strong>.</li>'; |
| return; |
| } |
| ul.innerHTML = list |
| .map((r) => { |
| const on = |
| ui.view === 'archive' |
| ? r.id === ui.archivePreviewId |
| ? ' on' |
| : '' |
| : r.id === store.activeId |
| ? ' on' |
| : ''; |
| const collecting = ui.collecting && store.activeId === r.id; |
| const statusCls = r.status === 'saved' ? 'rep-status-saved' : 'rep-status-draft'; |
| const statusLbl = r.status === 'saved' ? 'Arquivado' : 'Ativo'; |
| const { secondary, when, empty } = sidebarCardMeta(r); |
| const archiveBtn = |
| r.status !== 'saved' |
| ? `<button type="button" class="rep-report-act rep-report-archive" data-rep-archive="${attrEsc(r.id)}" title="Arquivar relatório" aria-label="Arquivar relatório">${ICON_ARCHIVE}</button>` |
| : ''; |
| const restoreBtn = |
| r.status === 'saved' |
| ? `<button type="button" class="rep-report-act rep-report-restore" data-rep-restore="${attrEsc(r.id)}" title="Restaurar para Ativos" aria-label="Restaurar para Ativos">${ICON_RESTORE}</button>` |
| : ''; |
| return `<li class="rep-report-li${on}${empty ? ' rep-report-empty' : ''}${collecting ? ' rep-report-collecting' : ''}"> |
| <button type="button" class="rep-report-btn${on}" data-rep-id="${r.id}"> |
| <span class="rep-report-btn-inner"> |
| <span class="nm">${esc(r.name)}</span> |
| <span class="rep-report-stats">${esc(secondary)}</span> |
| <span class="rep-report-foot"> |
| <span class="rep-status-pill ${statusCls}">${statusLbl}</span> |
| <span class="rep-report-when">${esc(when)}</span> |
| </span> |
| </span> |
| </button> |
| <div class="rep-report-actions">${archiveBtn}${restoreBtn}<button type="button" class="rep-report-act rep-report-del" data-rep-del="${attrEsc(r.id)}" title="Excluir relatório" aria-label="Excluir relatório">${ICON_TRASH}</button></div> |
| </li>`; |
| }) |
| .join(''); |
| ul.querySelectorAll('[data-rep-id]').forEach((btn) => { |
| btn.onclick = () => selectReport(btn.dataset.repId); |
| }); |
| ul.querySelectorAll('[data-rep-del]').forEach((btn) => { |
| btn.onclick = (ev) => { |
| ev.stopPropagation(); |
| deleteReport(btn.dataset.repDel); |
| }; |
| }); |
| ul.querySelectorAll('[data-rep-archive]').forEach((btn) => { |
| btn.onclick = (ev) => { |
| ev.stopPropagation(); |
| archiveReport(btn.dataset.repArchive); |
| }; |
| }); |
| ul.querySelectorAll('[data-rep-restore]').forEach((btn) => { |
| btn.onclick = (ev) => { |
| ev.stopPropagation(); |
| restoreReport(btn.dataset.repRestore); |
| }; |
| }); |
| } |
|
|
| function closeArchivePreview() { |
| ui.archivePreviewId = null; |
| render(); |
| } |
|
|
| function openArchivePreview(id) { |
| const r = store.reports.find((x) => x.id === id && x.status === 'saved'); |
| if (!r) return; |
| ui.archivePreviewId = id; |
| render(); |
| } |
|
|
| function renderArchivePreview(r) { |
| const el = document.getElementById('rep-main'); |
| if (!el) return; |
| const summary = reportSummaryText(r); |
| const savedAt = r.savedAt || r.updatedAt; |
| const savedLbl = savedAt |
| ? new Date(savedAt).toLocaleString('pt-BR', { |
| day: '2-digit', |
| month: 'short', |
| hour: '2-digit', |
| minute: '2-digit', |
| }) |
| : ''; |
| const rows = reportRowEntries(r); |
| el.innerHTML = ` |
| <div class="rep-preview-panel"> |
| <div class="rep-report-hd"> |
| <div class="rep-report-hd-top"> |
| <h3 class="rep-preview-title">${esc(r.name)}</h3> |
| <span class="rep-badge-saved">Salvo</span> |
| <span class="rep-progress">${esc(summary)}${savedLbl ? ' · ' + esc(savedLbl) : ''}</span> |
| </div> |
| <div class="rep-preview-actions"> |
| <button type="button" class="btn-upd" id="rep-preview-back">Voltar à lista</button> |
| <button type="button" class="btn-upd primary" id="rep-preview-restore">Restaurar para Ativos</button> |
| </div> |
| </div> |
| <p class="rep-archive-lead">Modo leitura — restaure para editar em <strong>Ativos</strong>.</p> |
| <div class="rep-table-single panel lista-panel rep-preview-table"> |
| <div class="tablewrap"> |
| <table>${previewTableHead()}<tbody>${rows.length ? rows.map((e) => repPreviewRowHtml(e)).join('') : '<tr><td colspan="9" class="rep-empty-row">Sem itens.</td></tr>'}</tbody></table> |
| </div> |
| </div> |
| </div>`; |
| el.querySelector('#rep-preview-back')?.addEventListener('click', closeArchivePreview); |
| el.querySelector('#rep-preview-restore')?.addEventListener('click', () => restoreReport(r.id)); |
| } |
|
|
| function renderArchiveMain() { |
| const el = document.getElementById('rep-main'); |
| if (!el) return; |
| if (ui.archivePreviewId) { |
| const pr = store.reports.find((x) => x.id === ui.archivePreviewId && x.status === 'saved'); |
| if (pr) { |
| renderArchivePreview(pr); |
| return; |
| } |
| ui.archivePreviewId = null; |
| } |
| const saved = [...store.reports.filter((r) => r.status === 'saved')].sort((a, b) => { |
| const ta = new Date(a.savedAt || a.updatedAt || 0).getTime(); |
| const tb = new Date(b.savedAt || b.updatedAt || 0).getTime(); |
| return tb - ta; |
| }); |
| if (!saved.length) { |
| el.innerHTML = |
| '<div class="rep-empty rep-archive-empty"><p>Nenhum relatório arquivado.</p><p class="rep-hint">Use o ícone de arquivo na sidebar de <strong>Ativos</strong> para arquivar um relatório.</p></div>'; |
| return; |
| } |
| el.innerHTML = ` |
| <div class="rep-archive-panel"> |
| <div class="rep-archive-hd"> |
| <p class="rep-archive-lead">${saved.length === 1 ? '1 relatório arquivado' : `${saved.length} relatórios arquivados`} — revise em modo leitura ou restaure para editar em <strong>Ativos</strong>.</p> |
| </div> |
| <div class="rep-archive-list"> |
| <table> |
| <thead><tr><th>Nome</th><th>Salvo em</th><th>Itens</th><th>Valor est.</th><th>Ações</th></tr></thead> |
| <tbody>${saved |
| .map((r) => { |
| const n = reportItemCount(r); |
| const val = reportValueRs(r); |
| const dt = new Date(r.savedAt || r.updatedAt).toLocaleString('pt-BR', { |
| day: '2-digit', |
| month: 'short', |
| hour: '2-digit', |
| minute: '2-digit', |
| }); |
| return `<tr> |
| <td><strong>${esc(r.name)}</strong></td> |
| <td>${esc(dt)}</td> |
| <td>${n}</td> |
| <td>${val > 0 && deps.fmtBRL ? esc(deps.fmtBRL(val)) : '—'}</td> |
| <td class="rep-archive-actions"> |
| <button type="button" class="chip-btn" data-rep-open="${r.id}">Revisar</button> |
| <button type="button" class="chip-btn" data-rep-restore="${r.id}">Restaurar</button> |
| <button type="button" class="chip-btn" data-rep-del-archive="${r.id}">Excluir</button> |
| </td> |
| </tr>`; |
| }) |
| .join('')}</tbody> |
| </table> |
| </div> |
| </div>`; |
| el.querySelectorAll('[data-rep-open]').forEach((btn) => { |
| btn.onclick = () => openArchivePreview(btn.dataset.repOpen); |
| }); |
| el.querySelectorAll('[data-rep-restore]').forEach((btn) => { |
| btn.onclick = () => restoreReport(btn.dataset.repRestore); |
| }); |
| el.querySelectorAll('[data-rep-del-archive]').forEach((btn) => { |
| btn.onclick = () => deleteReport(btn.dataset.repDelArchive); |
| }); |
| } |
|
|
| function renderWorkMain() { |
| const el = document.getElementById('rep-main'); |
| if (!el) return; |
| const r = activeReport(); |
| if (!r || r.status === 'saved') { |
| const archN = store.reports.filter((x) => x.status === 'saved').length; |
| const hint = |
| archN > 0 |
| ? '<p class="rep-hint">Há relatórios em <strong>Arquivados</strong> — restaure para editar ou crie um novo rascunho.</p>' |
| : '<p class="rep-hint">Use <strong>+ Novo relatório</strong> ou colete itens em Segmentação/Árvore.</p>'; |
| el.innerHTML = `<div class="rep-empty"><p>Nenhum rascunho ativo.</p>${hint}</div>`; |
| return; |
| } |
|
|
| const savedBadge = ''; |
|
|
| if (!r.items.length) { |
| el.innerHTML = ` |
| <div class="rep-report-hd"> |
| <div class="rep-report-hd-top"> |
| <input type="text" class="rep-title-in rep-title-hero" id="rep-title-in" value="${esc(r.name)}" aria-label="Nome"> |
| ${savedBadge} |
| <span class="rep-save-badge" id="rep-save-badge">Autosave</span> |
| </div> |
| </div> |
| <div class="rep-empty rep-cta"> |
| <p class="rep-empty-lead">Monte a lista em <strong>Segmentação</strong> ou <strong>Árvore</strong>.</p> |
| <div class="rep-empty-flow" aria-hidden="true"> |
| <span>Seg / Árvore</span><span class="rep-empty-arrow">→</span><span>+</span><span class="rep-empty-arrow">→</span><span>Relatório</span> |
| </div> |
| <div class="rep-empty-actions"> |
| <button type="button" class="btn-upd primary" id="rep-cta-collect">Ativar modo coleta</button> |
| <button type="button" class="btn-upd" data-rep-cta-page="explorar">Ir para Segmentação</button> |
| <button type="button" class="btn-upd" data-rep-cta-page="arvore">Ir para Árvore</button> |
| </div> |
| </div>`; |
| wireTitleInput(el.querySelector('#rep-title-in'), r); |
| wireEmptyCtas(el, r); |
| return; |
| } |
|
|
| const summary = reportSummaryText(r); |
| el.innerHTML = ` |
| <div class="rep-work-inner"> |
| <div class="rep-report-hd"> |
| <div class="rep-report-hd-top"> |
| <input type="text" class="rep-title-in rep-title-hero" id="rep-title-in" value="${esc(r.name)}" aria-label="Nome"> |
| ${savedBadge} |
| <span class="rep-progress" id="rep-counts">${esc(summary)}</span> |
| </div> |
| <div class="rep-report-hd-actions"> |
| <input type="search" id="rep-table-search" placeholder="Buscar código, descrição, família…" value="${esc(ui.tableSearch)}"> |
| <span class="rep-save-badge" id="rep-save-badge">Autosave</span> |
| <span class="rep-toolbar-secondary"> |
| <button type="button" class="btn-upd rep-hd-btn" id="rep-export-csv" title="Exportar CSV">CSV</button> |
| </span> |
| </div> |
| </div> |
| <div class="rep-seg-nav-wrap"> |
| <div class="rep-seg-nav" id="rep-seg-nav" role="tablist" aria-label="Segmento e pedidos abertos"></div> |
| </div> |
| <div class="rep-table-single panel lista-panel"> |
| <div class="tablewrap"> |
| <table>${tableHead()}<tbody id="rep-tbody"></tbody></table> |
| </div> |
| </div> |
| </div>`; |
|
|
| wireTitleInput(el.querySelector('#rep-title-in'), r); |
| el.querySelector('#rep-export-csv')?.addEventListener('click', exportCsv); |
| el.querySelector('#rep-table-search')?.addEventListener('input', (e) => { |
| ui.tableSearch = e.target.value; |
| renderTableOnly(); |
| }); |
|
|
| renderSegNav(); |
| renderTableOnly(); |
| } |
|
|
| function renderMain() { |
| if (ui.view === 'archive') renderArchiveMain(); |
| else renderWorkMain(); |
| } |
|
|
| function exportCsv() { |
| const r = activeReport(); |
| if (!r) return; |
| const rows = reportRowEntries(r); |
| const head = [ |
| 'codigo', |
| 'descricao', |
| 'seg', |
| 'familia', |
| 'rop', |
| 'qty_pedido', |
| 'qty_sugerida', |
| 'cob_atual', |
| 'cob_projetada', |
| 'alvo', |
| 'valor_rs', |
| 'nota', |
| ]; |
| const lines = [head.join(';')]; |
| for (const { x, s, ri } of rows) { |
| const proj = projectedCob(x, ri.qty); |
| lines.push( |
| [ |
| x.p, |
| (x.d || '').replace(/[;\n]/g, ' '), |
| itemSeg(x), |
| (x.fam || '').replace(/[;\n]/g, ' '), |
| x.rop ?? '', |
| ri.qty, |
| ri.qtySug, |
| x.cob ?? '', |
| proj == null ? '' : proj.toFixed(2), |
| s ? s.alvoRef : '', |
| Math.round(ri.qty * (x.uc || 0)), |
| (ri.note || '').replace(/[;\n]/g, ' '), |
| ].join(';') |
| ); |
| } |
| const blob = new Blob(['\ufeff' + lines.join('\n')], { type: 'text/csv;charset=utf-8' }); |
| const a = document.createElement('a'); |
| a.href = URL.createObjectURL(blob); |
| a.download = (r.name || 'relatorio').replace(/[^\w\-]+/g, '_').slice(0, 40) + '.csv'; |
| a.click(); |
| URL.revokeObjectURL(a.href); |
| } |
|
|
| function scheduleServerSave() { |
| if (!ui.serverAvailable) return; |
| setSaveStatus('pending'); |
| clearTimeout(serverTimer); |
| serverTimer = setTimeout(() => flushServerSave(false), 800); |
| } |
|
|
| async function flushServerSave(manual) { |
| if (!currentUser || !ui.serverAvailable) { |
| if (manual) alert('Servidor indisponível — dados só no navegador.'); |
| return; |
| } |
| if (saveInFlight) { |
| saveQueued = true; |
| updateSaveBadge(); |
| return; |
| } |
| saveInFlight = true; |
| saveQueued = false; |
| const revAtSend = storeRev; |
| setSaveStatus('saving'); |
| const payload = { version: 1, activeId: store.activeId, reports: store.reports }; |
| let needsRetry = false; |
| try { |
| const res = await fetch( |
| '/api/rep-reports', |
| apiFetchOpts({ |
| method: 'PUT', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(payload), |
| }) |
| ); |
| const data = await res.json().catch(() => ({})); |
| if (res.status === 401) { |
| ui.serverAvailable = false; |
| setSaveStatus('idle'); |
| if (manual) alert('Sessão expirada — faça login novamente.'); |
| return; |
| } |
| if (res.ok && data.reports) { |
| if (storeRev > revAtSend) { |
| needsRetry = true; |
| } else if (applyServerStore(data.reports, revAtSend)) { |
| ui.saveMode = 'server'; |
| saveLocal(); |
| setSaveStatus('saved'); |
| if (manual) render(); |
| else if (val('curPage') === 'relatorio') renderTableOnly(); |
| } |
| return; |
| } |
| setSaveStatus('idle'); |
| if (manual) alert(data.error || data.detail || 'Falha ao salvar no servidor'); |
| } catch (_) { |
| setSaveStatus('idle'); |
| if (manual) alert('Servidor indisponível'); |
| } finally { |
| saveInFlight = false; |
| if (needsRetry || saveQueued || storeRev > revAtSend) { |
| saveQueued = false; |
| clearTimeout(serverTimer); |
| serverTimer = setTimeout(() => flushServerSave(false), 0); |
| } else { |
| updateSaveBadge(); |
| } |
| } |
| } |
|
|
| function pushToServer(manual) { |
| return flushServerSave(manual); |
| } |
|
|
| async function pullFromServer() { |
| if (!currentUser) return false; |
| try { |
| const res = await fetch('/api/rep-reports', apiFetchOpts()); |
| if (res.status === 401) { |
| ui.serverAvailable = false; |
| return false; |
| } |
| if (!res.ok) return false; |
| const data = await res.json(); |
| if (!Array.isArray(data.reports)) return false; |
| const keepActive = store.activeId; |
| store = { |
| activeId: keepActive, |
| reports: preserveLocalDecisions(data.reports), |
| }; |
| if (keepActive && !store.reports.some((r) => r.id === keepActive)) store.activeId = null; |
| migrateStore(); |
| saveLocal(); |
| ui.serverAvailable = true; |
| ui.saveMode = 'server'; |
| return true; |
| } catch (_) { |
| return false; |
| } |
| } |
|
|
| async function detectServer() { |
| if (!currentUser) { |
| ui.serverAvailable = false; |
| return; |
| } |
| try { |
| const res = await fetch('/api/status', apiFetchOpts()); |
| ui.serverAvailable = res.ok; |
| } catch (_) { |
| ui.serverAvailable = false; |
| } |
| } |
|
|
| async function onUserReady(username) { |
| const user = (username || '').trim().toLowerCase(); |
| if (!user) return; |
| clearTimeout(serverTimer); |
| storeRev = 0; |
| saveInFlight = false; |
| saveQueued = false; |
| saveStatus = 'idle'; |
| currentUser = user; |
| store = { activeId: null, reports: [] }; |
| ui.collecting = false; |
| ui.lastWorkActiveId = null; |
| ui.archivePreviewId = null; |
| ui.view = 'work'; |
| loadLocal(user); |
| await detectServer(); |
| const pulled = await pullFromServer(); |
| if (!pulled && !store.reports.length) { |
| maybeImportLegacyLocal(); |
| if (store.reports.length) { |
| saveLocal(); |
| scheduleServerSave(); |
| } |
| } |
| render(); |
| renderHeaderBanner(); |
| refreshPickCells(); |
| } |
|
|
| function onUserLogout() { |
| clearTimeout(serverTimer); |
| storeRev = 0; |
| saveInFlight = false; |
| saveQueued = false; |
| saveStatus = 'idle'; |
| pickPending = null; |
| currentUser = null; |
| store = { activeId: null, reports: [] }; |
| ui.serverAvailable = false; |
| ui.collecting = false; |
| ui.saveMode = 'local'; |
| } |
|
|
| function loadSidebarCollapsed() { |
| try { |
| return localStorage.getItem(LS_SIDEBAR_KEY) === '1'; |
| } catch (_) { |
| return false; |
| } |
| } |
|
|
| function applySidebarCollapsed() { |
| const app = document.getElementById('rep-app'); |
| const btn = document.getElementById('rep-sidebar-toggle'); |
| if (app) app.classList.toggle('rep-sidebar-collapsed', !!ui.sidebarCollapsed); |
| if (btn) { |
| btn.textContent = ui.sidebarCollapsed ? '»' : '«'; |
| btn.title = ui.sidebarCollapsed ? 'Expandir rascunhos' : 'Recolher rascunhos'; |
| btn.setAttribute('aria-expanded', ui.sidebarCollapsed ? 'false' : 'true'); |
| } |
| } |
|
|
| function toggleSidebarCollapsed() { |
| ui.sidebarCollapsed = !ui.sidebarCollapsed; |
| try { |
| localStorage.setItem(LS_SIDEBAR_KEY, ui.sidebarCollapsed ? '1' : '0'); |
| } catch (_) {} |
| applySidebarCollapsed(); |
| } |
|
|
| function updateViewTabs() { |
| const draftN = store.reports.filter((r) => r.status !== 'saved').length; |
| const archN = store.reports.filter((r) => r.status === 'saved').length; |
| document.querySelectorAll('[data-rep-view]').forEach((btn) => { |
| const v = btn.dataset.repView; |
| btn.classList.toggle('on', v === ui.view); |
| const full = btn.querySelector('.rep-view-full'); |
| if (full) { |
| const n = v === 'archive' ? archN : draftN; |
| full.innerHTML = |
| v === 'archive' |
| ? `Arquivados<span class="rep-view-tab-count">(${n})</span>` |
| : `Ativos<span class="rep-view-tab-count">(${n})</span>`; |
| } |
| }); |
| const title = document.getElementById('rep-sidebar-title'); |
| if (title) title.textContent = ui.view === 'archive' ? 'Arquivados' : 'Relatórios'; |
| const newBtn = document.getElementById('rep-new-report'); |
| if (newBtn) newBtn.hidden = ui.view === 'archive'; |
| const sidebar = document.getElementById('rep-sidebar'); |
| if (sidebar) sidebar.classList.toggle('rep-sidebar-archive-mode', ui.view === 'archive'); |
| applySidebarCollapsed(); |
| } |
|
|
| function render() { |
| updateViewTabs(); |
| renderSidebar(); |
| renderMain(); |
| renderHeaderBanner(); |
| const page = document.getElementById('page-relatorio'); |
| if (page) page.classList.toggle('rep-view-archive', ui.view === 'archive'); |
| updateSaveBadge(); |
| } |
|
|
| function onFilterChange() { |
| if (val('curPage') === 'relatorio') render(); |
| } |
|
|
| function setActiveId(id) { |
| if (!id) return; |
| const r = store.reports.find((x) => x.id === id); |
| if (!r) return; |
| if (r.status === 'saved') { |
| ui.view = 'archive'; |
| ui.archivePreviewId = id; |
| store.activeId = null; |
| } else { |
| store.activeId = id; |
| ui.lastWorkActiveId = id; |
| ui.archivePreviewId = null; |
| if (ui.view === 'archive') ui.view = 'work'; |
| if (isCollectPage()) { |
| enterCollectMode(id); |
| return; |
| } |
| } |
| persistStore(); |
| deps.syncUrl?.(); |
| renderHeaderBanner(); |
| refreshPickCells(); |
| } |
|
|
| function getActiveId() { |
| return store.activeId; |
| } |
|
|
| function init(initDeps) { |
| deps = initDeps; |
| ui.sidebarCollapsed = loadSidebarCollapsed(); |
| if (initWired) { |
| renderHeaderBanner(); |
| return; |
| } |
| initWired = true; |
| ui.collecting = false; |
| ui.lastWorkActiveId = null; |
| wireRepTableClicks(); |
| wireRepTableSort(); |
| renderHeaderBanner(); |
| document.getElementById('rep-sidebar-toggle')?.addEventListener('click', toggleSidebarCollapsed); |
| document.getElementById('rep-new-report')?.addEventListener('click', () => openNewModal(true)); |
| document.getElementById('rep-new-cancel')?.addEventListener('click', () => cancelPickPending()); |
| document.getElementById('rep-new-save')?.addEventListener('click', () => { |
| const name = document.getElementById('rep-new-name')?.value || ''; |
| const fn = pickPending; |
| pickPending = null; |
| createReport(name); |
| openNewModal(false); |
| closePickModal(); |
| if (fn) fn(); |
| }); |
| document.getElementById('rep-new-modal')?.addEventListener('click', (e) => { |
| if (e.target.id === 'rep-new-modal') cancelPickPending(); |
| }); |
| document.getElementById('rep-pick-cancel')?.addEventListener('click', () => cancelPickPending()); |
| document.getElementById('rep-pick-new')?.addEventListener('click', () => { |
| closePickModal(); |
| openNewModal(true, pickPending); |
| }); |
| document.getElementById('rep-pick-modal')?.addEventListener('click', (e) => { |
| if (e.target.id === 'rep-pick-modal') cancelPickPending(); |
| }); |
| document.getElementById('rep-delete-cancel')?.addEventListener('click', () => closeDeleteModal()); |
| document.getElementById('rep-delete-confirm')?.addEventListener('click', () => confirmDeleteReport()); |
| document.getElementById('rep-delete-modal')?.addEventListener('click', (e) => { |
| if (e.target.id === 'rep-delete-modal') closeDeleteModal(); |
| }); |
| document.getElementById('header-report-exit')?.addEventListener('click', () => { |
| setHeaderReportMenuOpen(false); |
| if (ui.collecting) { |
| store.activeId = null; |
| ui.lastWorkActiveId = null; |
| persistStore(); |
| deps.syncUrl?.(); |
| exitCollectMode(); |
| } else { |
| const d = activeDraftReport(); |
| if (d) enterCollectMode(d.id); |
| else if (val('curPage') === 'relatorio') goPage('explorar'); |
| } |
| }); |
| const headerRepBtn = document.getElementById('header-report-name-btn'); |
| if (headerRepBtn && !headerRepBtn._repWired) { |
| headerRepBtn._repWired = true; |
| headerRepBtn.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| setHeaderReportMenuOpen(!headerReportMenuOpen); |
| }); |
| } |
| if (!document._headerRepMenuWired) { |
| document._headerRepMenuWired = true; |
| document.addEventListener('click', (e) => { |
| if (!headerReportMenuOpen) return; |
| const sw = document.querySelector('.header-report-switcher'); |
| if (sw && !sw.contains(e.target)) setHeaderReportMenuOpen(false); |
| }); |
| document.addEventListener('keydown', (e) => { |
| if (e.key === 'Escape' && headerReportMenuOpen) setHeaderReportMenuOpen(false); |
| }); |
| } |
| document.querySelectorAll('[data-rep-view]').forEach((btn) => { |
| btn.onclick = () => setView(btn.dataset.repView); |
| }); |
| const sidebarSearch = document.getElementById('rep-sidebar-search'); |
| if (sidebarSearch && !sidebarSearch._repWired) { |
| sidebarSearch._repWired = true; |
| sidebarSearch.addEventListener('input', (e) => { |
| ui.sidebarSearch = e.target.value; |
| renderSidebar(); |
| }); |
| } |
| } |
|
|
| global.AxoRep = { |
| init, |
| onUserReady, |
| onUserLogout, |
| render, |
| onFilterChange, |
| setActiveId, |
| getActiveId, |
| getView, |
| setView, |
| getSegFilter, |
| setSegFilter, |
| createReport, |
| isCollecting, |
| getActiveReport, |
| isInReport, |
| enterCollectMode, |
| exitCollectMode, |
| addItem, |
| removeItem, |
| toggleItem, |
| pickCellHtml, |
| refreshPickCells, |
| refreshHeaderBanner: renderHeaderBanner, |
| promptPickOrCreateReport, |
| }; |
| })(typeof window !== 'undefined' ? window : globalThis); |
|
|