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
+
+
+
| Depósito | Lado | Endereço | SKU | Tipo | Quantidade |
@@ -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
| Data | Registros |
@@ -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
-
-
+