axo / scripts /rep_workspace.js
Gabriel Sapucaia
Upload folder using huggingface_hub
4f8b838 verified
Raw
History Blame Contribute Delete
66.3 kB
/**
* AxoRep — relatórios de compra: coleta por hover (+/−) + revisão/aprovação.
*/
(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, '&amp;').replace(/"/g, '&quot;');
}
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);