diff --git a/app.js b/app.js index a2ad09d..36f38ba 100644 --- a/app.js +++ b/app.js @@ -32,6 +32,10 @@ function storageGetJSON(key, fallback) { } } +function storageSetJSON(key, value) { + return storageSet(key, JSON.stringify(value)); +} + const el = { paleteIncompletoToggle: document.getElementById('paleteIncompletoToggle'), @@ -62,6 +66,7 @@ const el = { consultaChaoBody: document.querySelector('#consultaChaoTable tbody'), consultaSkuAreaBody: document.querySelector('#consultaSkuAreaTable tbody'), consultaFilterForm: document.getElementById('consultaFilterForm'), + consultaRefreshBtn: document.getElementById('consultaRefreshBtn'), consultaDepositoFilter: document.getElementById('consultaDepositoFilter'), consultaSideFilter: document.getElementById('consultaSideFilter'), consultaSkuSearch: document.getElementById('consultaSkuSearch'), @@ -181,6 +186,41 @@ if (el.themeToggleBtn) el.themeToggleBtn.textContent = darkModeEnabled ? '☀️ let currentUser = null; +const TURNO_SNAPSHOT_STORAGE_KEY = 'wmss_turno_snapshot_v2'; + +function normalizePerfil(value) { + const perfil = normalizeText(value); + if (['MASTER', 'MESTRE'].includes(perfil)) return 'MASTER'; + return 'COMUM'; +} + +function splitRowsByCentro(rows = []) { + return rows.reduce((acc, row) => { + const centro = getDepositoFromArea(row.area); + if (!acc[centro]) acc[centro] = []; + acc[centro].push(row); + return acc; + }, {}); +} + +function loadTurnoSnapshotFromStorage() { + const saved = storageGetJSON(TURNO_SNAPSHOT_STORAGE_KEY, null); + if (!saved || !Array.isArray(saved.rows)) return; + const rows = saved.rows + .filter((row) => row && row.area && Number(row.sku) > 0 && Number(row.paletes) >= 0) + .map((row) => ({ + area: normalizeAreaCode(row.area), + sku: Number(row.sku), + tipo: normalizeText(row.tipo) || 'PL2', + paletes: Number(row.paletes) + })); + if (!rows.length) return; + turnoSnapshots = [{ + created_at: saved.created_at || new Date().toISOString(), + rows, + centros: splitRowsByCentro(rows) + }]; +} function canAccessRole(requiredRole) { if (!requiredRole) return true; @@ -237,7 +277,7 @@ function setupLogin() { return; } - const perfil = normalizeText(auth.perfil || 'COMUM'); + const perfil = normalizePerfil(auth.perfil || 'COMUM'); currentUser = { id: auth.usuario || user, role: perfil === 'MASTER' ? 'master' : 'common' @@ -456,15 +496,16 @@ function renderUsersTable() { el.usersTableBody.innerHTML = ''; cache.users.forEach((u) => { const tr = document.createElement('tr'); - tr.innerHTML = `${u.usuario}${u.nome || ''}${u.perfil}${u.ativo ? 'SIM' : 'NÃO'}`; + tr.innerHTML = `${u.usuario}${u.nome || ''}${normalizePerfil(u.perfil)}${u.ativo ? 'SIM' : 'NÃO'}`; tr.querySelector('[data-edit]')?.addEventListener('click', () => { if (!el.userForm) return; el.userForm.usuario.value = u.usuario || ''; el.userForm.nome.value = u.nome || ''; el.userForm.senha.value = ''; - el.userForm.perfil.value = u.perfil || 'COMUM'; + el.userForm.perfil.value = normalizePerfil(u.perfil || 'COMUM'); el.userForm.ativo.checked = Boolean(u.ativo); - setStatus(el.userStatus, `Editando usuário ${u.usuario}. Informe nova senha para atualizar.`, ''); + el.userForm.dataset.editingUser = u.usuario || ''; + setStatus(el.userStatus, `Editando usuário ${u.usuario}. Senha é opcional para alterar perfil/status.`, ''); }); el.usersTableBody.appendChild(tr); }); @@ -481,7 +522,7 @@ async function loadUsers() { .select('usuario, nome, perfil, ativo') .order('usuario', { ascending: true }); if (error) throw error; - cache.users = data ?? []; + cache.users = (data ?? []).map((u) => ({ ...u, perfil: normalizePerfil(u.perfil) })); renderUsersTable(); } @@ -494,20 +535,36 @@ async function handleUserSubmit(event) { const usuario = normalizeText(formData.get('usuario')); const nome = String(formData.get('nome') || '').trim(); const senha = String(formData.get('senha') || '').trim(); - const perfil = normalizeText(formData.get('perfil') || 'COMUM'); + const perfil = normalizePerfil(formData.get('perfil') || 'COMUM'); const ativo = formData.get('ativo') === 'on'; + const editingUser = normalizeText(event.target.dataset.editingUser || ''); + const isEditing = Boolean(editingUser && editingUser === usuario); - if (!usuario || !senha) return setStatus(el.userStatus, 'Informe usuário e senha.', 'error'); + if (!usuario) return setStatus(el.userStatus, 'Informe usuário.', 'error'); + if (!senha && !isEditing) return setStatus(el.userStatus, 'Informe senha para novo usuário.', 'error'); if (!['MASTER', 'COMUM'].includes(perfil)) return setStatus(el.userStatus, 'Perfil inválido.', 'error'); - const payload = { usuario, nome, senha, perfil, ativo }; + const payload = { usuario, nome, perfil, ativo }; + if (senha) payload.senha = senha; + try { - const { error } = await supabaseClient - .from('wmss_users') - .upsert(payload, { onConflict: 'usuario' }); - if (error) throw error; + const perfilCandidates = perfil === 'MASTER' ? ['MASTER', 'MESTRE'] : ['COMUM']; + let lastError = null; + for (const perfilCandidate of perfilCandidates) { + const { error } = await supabaseClient + .from('wmss_users') + .upsert({ ...payload, perfil: perfilCandidate }, { onConflict: 'usuario' }); + if (!error) { + lastError = null; + break; + } + lastError = error; + if (!String(error.message || '').includes('wmss_users_perfil_check')) break; + } + if (lastError) throw lastError; event.target.reset(); if (event.target.ativo) event.target.ativo.checked = true; + delete event.target.dataset.editingUser; await loadUsers(); setStatus(el.userStatus, 'Usuário salvo com sucesso.', 'success'); } catch (error) { @@ -1697,8 +1754,7 @@ function setupContagem() { el.contagemApplyBtn?.addEventListener('click', applyContagemToConsulta); el.contagemForm?.addEventListener('submit', (event) => { event.preventDefault(); - renderContagemTable(); - setStatus(el.contagemStatus, 'Cálculo atualizado.', 'success'); + applyContagemToConsulta(); }); el.contagemExportBtn?.addEventListener('click', exportContagemExcel); } @@ -1845,10 +1901,12 @@ function consolidarPorChave(rows) { function saveTurnoSnapshot(snapshotRows) { const item = { created_at: new Date().toISOString(), - rows: snapshotRows + rows: snapshotRows, + centros: splitRowsByCentro(snapshotRows) }; - turnoSnapshots = [item, ...turnoSnapshots].slice(0, 3); + turnoSnapshots = [item]; + storageSetJSON(TURNO_SNAPSHOT_STORAGE_KEY, item); renderTurnoHistory(); } @@ -1856,18 +1914,20 @@ function renderTurnoHistory() { if (!el.turnoHistoryBody) return; el.turnoHistoryBody.innerHTML = ''; - if (!turnoSnapshots.length) { + const latest = turnoSnapshots[0]; + if (!latest) { const tr = document.createElement('tr'); tr.innerHTML = 'Sem snapshots salvos.'; el.turnoHistoryBody.appendChild(tr); return; } - turnoSnapshots.forEach((snap) => { - const tr = document.createElement('tr'); - tr.innerHTML = `${new Date(snap.created_at).toLocaleString('pt-BR')}${snap.rows.length}`; - el.turnoHistoryBody.appendChild(tr); - }); + const resumoCentros = Object.entries(latest.centros || {}) + .map(([centro, rows]) => `${centro}: ${rows.length}`) + .join(' | '); + const tr = document.createElement('tr'); + tr.innerHTML = `${new Date(latest.created_at).toLocaleString('pt-BR')}${latest.rows.length}${resumoCentros ? ` (${resumoCentros})` : ''}`; + el.turnoHistoryBody.appendChild(tr); } async function handleTurnoSubmit(event) { @@ -2522,6 +2582,10 @@ function setupConsulta() { el.mapaAreaSelect?.addEventListener('change', renderConsultaMapaHint); el.mapaPosicaoInput?.addEventListener('input', renderConsultaMapaHint); el.exportConsultaResumoPdfBtn?.addEventListener('click', exportConsultaResumoPDF); + el.consultaRefreshBtn?.addEventListener('click', async () => { + await loadAll(); + showFeedback('Consulta atualizada sem recarregar a página.'); + }); document.querySelectorAll('.consulta-subnav-btn').forEach((btn) => { btn.addEventListener('click', () => { document.querySelectorAll('.consulta-subnav-btn').forEach((b) => b.classList.remove('active')); @@ -2553,6 +2617,7 @@ function init() { setupPlanejamento(); setupOcupacao(); setupContagem(); + loadTurnoSnapshotFromStorage(); renderTurnoHistory(); el.turnoForm?.addEventListener('submit', handleTurnoSubmit); el.exportTurnoExcelBtn?.addEventListener('click', exportTurnoResultadoExcel); diff --git a/index.html b/index.html index 73b5a1a..f0c29b7 100644 --- a/index.html +++ b/index.html @@ -31,7 +31,7 @@

Controle de Estoque por Área

- + @@ -81,7 +81,7 @@

Cadastro de usuários

- +
+
+ +
@@ -190,9 +193,9 @@

Consulta: onde estão os produtos

-

Conferência por planilha (fim de turno)

-

Suba a planilha da contagem final do dia. O sistema salva esse snapshot e compara com o snapshot anterior.

-

Histórico local: mantém as últimas 3 atualizações para comparar dia atual x dia anterior sem sobrecarregar o banco.

+

MB51: carga de planilha por centro (fim de turno)

+

Suba a planilha MB51. O sistema guarda a última versão e só substitui quando uma nova planilha for enviada.

+

Os registros ficam separados por centros (Principal, Tissue, TTD, Lonil e Estrutura) no resumo abaixo.

-

Histórico de snapshots (últimos 3)

+

Última planilha MB51 salva

DepósitoLadoEndereçoSKUTipoQuantidade
@@ -228,7 +231,7 @@

Saída calculada por SKU

-
+

Planejamento de recebimento (NF)

Informe previsão de chegada por SKU para estimar ocupação e blocados que precisam ser desfeitos.

@@ -335,8 +338,7 @@

Contagem por rua/estrutura

- - +
DataRegistros