Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 87 additions & 22 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ function storageGetJSON(key, fallback) {
}
}

function storageSetJSON(key, value) {
return storageSet(key, JSON.stringify(value));
}


const el = {
paleteIncompletoToggle: document.getElementById('paleteIncompletoToggle'),
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -456,15 +496,16 @@ function renderUsersTable() {
el.usersTableBody.innerHTML = '';
cache.users.forEach((u) => {
const tr = document.createElement('tr');
tr.innerHTML = `<td>${u.usuario}</td><td>${u.nome || ''}</td><td>${u.perfil}</td><td>${u.ativo ? 'SIM' : 'NÃO'}</td><td><button type="button" class="secondary" data-edit="${u.usuario}">Editar</button></td>`;
tr.innerHTML = `<td>${u.usuario}</td><td>${u.nome || ''}</td><td>${normalizePerfil(u.perfil)}</td><td>${u.ativo ? 'SIM' : 'NÃO'}</td><td><button type="button" class="secondary" data-edit="${u.usuario}">Editar</button></td>`;
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);
});
Expand All @@ -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();
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -1845,29 +1901,33 @@ 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();
}

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 = '<td colspan="2">Sem snapshots salvos.</td>';
el.turnoHistoryBody.appendChild(tr);
return;
}

turnoSnapshots.forEach((snap) => {
const tr = document.createElement('tr');
tr.innerHTML = `<td>${new Date(snap.created_at).toLocaleString('pt-BR')}</td><td>${snap.rows.length}</td>`;
el.turnoHistoryBody.appendChild(tr);
});
const resumoCentros = Object.entries(latest.centros || {})
.map(([centro, rows]) => `${centro}: ${rows.length}`)
.join(' | ');
const tr = document.createElement('tr');
tr.innerHTML = `<td>${new Date(latest.created_at).toLocaleString('pt-BR')}</td><td>${latest.rows.length}${resumoCentros ? ` (${resumoCentros})` : ''}</td>`;
el.turnoHistoryBody.appendChild(tr);
}

async function handleTurnoSubmit(event) {
Expand Down Expand Up @@ -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'));
Expand Down Expand Up @@ -2553,6 +2617,7 @@ function init() {
setupPlanejamento();
setupOcupacao();
setupContagem();
loadTurnoSnapshotFromStorage();
renderTurnoHistory();
el.turnoForm?.addEventListener('submit', handleTurnoSubmit);
el.exportTurnoExcelBtn?.addEventListener('click', exportTurnoResultadoExcel);
Expand Down
20 changes: 11 additions & 9 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ <h1>Controle de Estoque por Área</h1>
<button class="tab-btn active" data-tab="cadastro" data-role="master">Cadastro</button>
<button class="tab-btn" data-tab="consulta">Consulta</button>
<button class="tab-btn" data-tab="expedicao" data-role="master">Conferência de turno</button>
<button class="tab-btn" data-tab="planejamento">Planejamento</button>
<button class="tab-btn" data-tab="planejamento" data-role="master">Planejamento</button>
<button class="tab-btn" data-tab="ocupacao" data-role="master">Ocupação</button>
<button class="tab-btn" data-tab="contagem">Contagem</button>
</nav>
Expand Down Expand Up @@ -81,7 +81,7 @@ <h2>Cadastro de usuários</h2>
<form id="userForm" class="grid form-grid">
<label>Usuário <input name="usuario" required placeholder="Ex.: 12345678" /></label>
<label>Nome <input name="nome" placeholder="Ex.: Operador turno A" /></label>
<label>Senha <input name="senha" type="password" required /></label>
<label>Senha <input name="senha" type="password" /></label>
<label>Perfil
<select name="perfil" required>
<option value="COMUM">COMUM</option>
Expand Down Expand Up @@ -176,6 +176,9 @@ <h2>Consulta: onde estão os produtos</h2>
<input id="consultaSkuSearch" placeholder="Ex.: 20104409" />
</label>
</form>
<div class="actions">
<button id="consultaRefreshBtn" type="button" class="secondary">Refresh informações</button>
</div>
<div class="table-wrapper">
<table id="consultaAreaTable">
<thead><tr><th>Depósito</th><th>Lado</th><th>Endereço</th><th>SKU</th><th>Tipo</th><th>Quantidade</th></tr></thead>
Expand All @@ -190,9 +193,9 @@ <h2>Consulta: onde estão os produtos</h2>

<section id="expedicao" class="tab-content" data-role="master">
<div class="card">
<h2>Conferência por planilha (fim de turno)</h2>
<p class="helper-text">Suba a planilha da contagem final do dia. O sistema salva esse snapshot e compara com o snapshot anterior.</p>
<p class="helper-text">Histórico local: mantém as últimas 3 atualizações para comparar dia atual x dia anterior sem sobrecarregar o banco.</p>
<h2>MB51: carga de planilha por centro (fim de turno)</h2>
<p class="helper-text">Suba a planilha MB51. O sistema guarda a última versão e só substitui quando uma nova planilha for enviada.</p>
<p class="helper-text">Os registros ficam separados por centros (Principal, Tissue, TTD, Lonil e Estrutura) no resumo abaixo.</p>
<form id="turnoForm" class="grid form-grid">
<label>
Planilha final (.xlsx/.xls/.csv)
Expand All @@ -208,7 +211,7 @@ <h2>Conferência por planilha (fim de turno)</h2>
</div>

<div class="card">
<h2>Histórico de snapshots (últimos 3)</h2>
<h2>Última planilha MB51 salva</h2>
<div class="table-wrapper">
<table id="turnoHistoryTable">
<thead><tr><th>Data</th><th>Registros</th></tr></thead>
Expand All @@ -228,7 +231,7 @@ <h2>Saída calculada por SKU</h2>
</div>
</section>

<section id="planejamento" class="tab-content">
<section id="planejamento" class="tab-content" data-role="master">
<div class="card">
<h2>Planejamento de recebimento (NF)</h2>
<p class="helper-text">Informe previsão de chegada por SKU para estimar ocupação e blocados que precisam ser desfeitos.</p>
Expand Down Expand Up @@ -335,8 +338,7 @@ <h2>Contagem por rua/estrutura</h2>
<button id="contagemLoadBtn" type="button" class="secondary">Carregar posições</button>
<button id="contagemImportBtn" type="button" class="secondary">Preencher pela planilha</button>
<button id="contagemClearChecksBtn" type="button" class="secondary">Limpar checkboxes (novo turno)</button>
<button type="submit">Calcular</button>
<button id="contagemApplyBtn" type="button">Atualizar consulta (confirmados)</button>
<button type="submit">Calcular (enviar para MB52)</button>
<button id="contagemExportBtn" type="button" class="secondary">Exportar Excel (todas as áreas)</button>
</div>
</form>
Expand Down