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
259 changes: 259 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
const SUPABASE_URL = "https://qfjghplxbtogshfjkawx.supabase.co";
const SUPABASE_KEY = "sb_publishable_rIcKdaflOvJ0DLTJDcOrxA_bpTGG2hA";
const supabaseClient = window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY);

const STAR_PRODUCTS = [
["20104425", "PANO SCOTT DURAMAX 58X03X08 BRANCO"],
["20092384", "GUARDANAPO SCALA 22X20 - 3600 UN"],
["20104429", "GUARD. SCOTT GRANDHOTEL FD 9X9X50 P"],
["20104430", "GUARD. SCOTT GRANDHOTEL FD 8X9X50 G"],
["20104313", "PANO SCOTT DURAMAX 58X01X24 BRANCO"],
["20104426", "GUARD. SCOTT DIA A DIA FS 6X12X50 P"],
["20104309", "LENCO FAC KLEENEX BLS FT 10X04X72 L4P3"],
["20092383", "GUARDANAPO SCALA 22X20 - CX 4800 UN"],
["20104422", "LENCO FAC KLEENEX BX FD 100X40 L100P80"],
["20092388", "GUARDAPANO NAPS 33X30 - 1800 UN"],
["20104421", "LENCO FAC KLEENEX BX FD 50X60 L60P50"],
["20092386", "GUARDANAPO NAPS 23X21,5 - 3600 UN"],
["20110078", "PANO SCALA 50X01X24 BRANCO"],
["90004710", "LENÇO UMEDECIDO MIMMO 40x24"],
["90004853", "LENCO UMED. NEVE TOQ SEDA 48X24"],
["90005082", "LENCO UMED. NEVE ON THE GO 16X24"],
["90005191", "LENCO UMED. NEVE 48X4X6 L4P3"],
["20091836", "TOAX430 3R 120F 3X6 SCALA PLUS MEGA"],
["20091835", "TOAX430 2R 100F 2X12 SCALA PLUS MPICOTE"],
["20091834", "TOAX430 2R 60F 2X12 SCALA PLUS REG"],
["20111061", "TOAX430 2R 120F 3X6 SCALA WARM UP"]
];

const tabButtons = document.querySelectorAll(".tab-btn");
const panels = document.querySelectorAll(".tab-panel");
const statusEl = document.getElementById("status");
const resumoImportacaoEl = document.getElementById("resumo-importacao");
const listaDtsEl = document.getElementById("lista-dts");
const importarBtnEl = document.getElementById("importar-sap");

const STORAGE_KEY = "dts-store-v2";
let dtStore = loadStore();

function loadStore() {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : {};
}

function saveStore() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(dtStore));
}

function setImportLoading(loading) {
importarBtnEl.disabled = loading;
importarBtnEl.textContent = loading ? "Processando..." : "Importar/Atualizar linhas SAP";
statusEl.classList.toggle("loading", loading);
}

tabButtons.forEach((btn) => {
btn.addEventListener("click", () => {
const target = btn.dataset.tab;
tabButtons.forEach((b) => b.classList.remove("active"));
panels.forEach((p) => p.classList.remove("active"));
btn.classList.add("active");
document.getElementById(target).classList.add("active");
});
});

function formatDateFromSAP(value) {
const clean = String(value || "").trim();
if (/^\d{2}\.\d{2}\.\d{4}$/.test(clean)) {
const [dd, mm, yyyy] = clean.split(".");
return `${yyyy}-${mm}-${dd}`;
}
return clean;
}

async function buscarPadraoPorSku(sku) {
const { data, error } = await supabaseClient
.from("produtos")
.select("sku, fardos_por_palete")
.eq("sku", sku)
.limit(1);

if (error || !data?.length) return null;
return Number(data[0].fardos_por_palete || 0);
}

async function parseSapLineToItem(line) {
const cols = line.split("\t").map((v) => v.trim());
if (cols.length < 14) return null;

const material = cols[6];
const qtd = Number(cols[5] || 0);
const padraoSupabase = await buscarPadraoPorSku(material);
const padrao = Number(padraoSupabase || cols[12] || 0);

return {
codCliente: cols[0],
codTransportadora: cols[1],
numeroTransporte: cols[2],
infoAgenda: cols[3],
nomeCliente: cols[4],
setorAtividade: cols[7],
clientePallet: cols[8],
agrupamentoRegional: cols[9],
qtdTeoricaConvertida: cols[10],
numeroNfe: cols[11],
qtdTeorica: cols[12],
dataAgendamento: formatDateFromSAP(cols[13]),
item: {
sku: material,
qtd,
padrao,
pltFechado: padrao > 0 ? Math.floor(qtd / padrao) : 0,
fracao: padrao > 0 ? qtd % padrao : qtd
}
};
}

async function importarLinhasSAP() {
const raw = document.getElementById("sap-linhas").value.trim();
if (!raw) {
statusEl.textContent = "Cole uma ou mais linhas do SAP.";
return;
}

const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
let importadas = 0;

setImportLoading(true);
statusEl.textContent = `Processando ${lines.length} linha(s)...`;

try {
for (const line of lines) {
const parsed = await parseSapLineToItem(line);
if (!parsed?.numeroTransporte) continue;

if (!dtStore[parsed.numeroTransporte]) {
dtStore[parsed.numeroTransporte] = {
codCliente: parsed.codCliente,
codTransportadora: parsed.codTransportadora,
numeroTransporte: parsed.numeroTransporte,
infoAgenda: parsed.infoAgenda,
nomeCliente: parsed.nomeCliente,
setorAtividade: parsed.setorAtividade,
clientePallet: parsed.clientePallet,
agrupamentoRegional: parsed.agrupamentoRegional,
qtdTeoricaConvertida: parsed.qtdTeoricaConvertida,
numeroNfe: parsed.numeroNfe,
qtdTeorica: parsed.qtdTeorica,
dataAgendamento: parsed.dataAgendamento,
itens: []
};
}

dtStore[parsed.numeroTransporte].itens.push(parsed.item);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace existing DT items instead of always appending

For DTs already present in dtStore, imports always append new items and never reconcile old ones. Re-importing the same SAP extract (or importing a corrected extract) therefore duplicates rows and inflates totals in both the summary table and printable report. The update path should clear or de-duplicate the DT item list before adding freshly parsed lines.

Useful? React with 👍 / 👎.

importadas += 1;
}

saveStore();
renderResumoImportacao();
statusEl.textContent = `${importadas} linha(s) importada(s) e agrupada(s) por DT.`;
} finally {
setImportLoading(false);
}
}

document.getElementById("importar-sap").addEventListener("click", importarLinhasSAP);

function renderResumoImportacao() {
resumoImportacaoEl.innerHTML = "";
const registros = Object.values(dtStore);

if (!registros.length) {
resumoImportacaoEl.innerHTML = '<tr><td colspan="4">Nenhuma DT importada ainda.</td></tr>';
return;
}

registros.forEach((dt) => {
const total = (dt.itens || []).reduce((acc, item) => acc + Number(item.qtd || 0), 0);
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${dt.numeroTransporte}</td>
<td>${dt.nomeCliente || "-"}</td>
<td>${dt.itens?.length || 0}</td>
<td>${total}</td>
`;
resumoImportacaoEl.appendChild(tr);
});
}

function pesquisarPorFinalDT() {
const term = document.getElementById("busca-dt").value.trim();
const allDts = Object.keys(dtStore);
const matches = allDts.filter((dt) => !term || dt.endsWith(term));

listaDtsEl.innerHTML = "";
if (!matches.length) {
listaDtsEl.innerHTML = '<option value="">Nenhuma DT encontrada</option>';
return;
}

matches.forEach((dt) => {
const option = document.createElement("option");
option.value = dt;
option.textContent = `${dt} - ${dtStore[dt].nomeCliente || "Sem cliente"}`;
listaDtsEl.appendChild(option);
});
}

document.getElementById("pesquisar-dt").addEventListener("click", pesquisarPorFinalDT);

function fillSheet(data) {
document.getElementById("sheet-dt").textContent = `DT: ${data.numeroTransporte || "-"}`;
document.getElementById("sheet-cliente").textContent = `Cliente: ${data.nomeCliente || "-"}`;
document.getElementById("sheet-data").textContent = `Data agendamento: ${data.dataAgendamento || "-"}`;

const tbody = document.getElementById("sheet-itens");
tbody.innerHTML = "";
let total = 0;

(data.itens || []).forEach((item) => {
total += Number(item.qtd || 0);
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${data.numeroTransporte || ""}</td>
<td>${item.sku || ""}</td>
Comment on lines +221 to +223

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Escape imported values before assigning tr.innerHTML

This row template injects item.sku (and nearby fields) directly into innerHTML even though the values come from pasted SAP text. If a pasted field contains HTML with an event handler, it will execute when the DT is rendered, turning import data into script execution in the operator’s browser. Render these values via textContent (or sanitize) instead of interpolating raw strings into HTML.

Useful? React with 👍 / 👎.

<td>${item.qtd || 0}</td>
<td>${item.padrao || 0}</td>
<td>${item.pltFechado || 0}</td>
<td>${item.fracao || 0}</td>
<td>☐</td>
<td>☐</td>
`;
tbody.appendChild(tr);
});

document.getElementById("sheet-total").textContent = total;
}

document.getElementById("carregar-dt").addEventListener("click", () => {
const dt = listaDtsEl.value;
if (!dt || !dtStore[dt]) {
alert("Selecione uma DT válida.");
return;
}
fillSheet(dtStore[dt]);
});

document.getElementById("imprimir").addEventListener("click", () => window.print());

function renderStarProducts() {
const body = document.getElementById("produtos-estrela-body");
STAR_PRODUCTS.forEach(([sku, descricao]) => {
const tr = document.createElement("tr");
tr.innerHTML = `<td>${sku}</td><td>${descricao}</td>`;
body.appendChild(tr);
});
}

renderResumoImportacao();
pesquisarPorFinalDT();
renderStarProducts();
128 changes: 128 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ZLES002 | Relatório de Paletização</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<header>
<h1>Painel Logístico</h1>
<p>Importação em lote do SAP, relatório por DT e produtos estrela.</p>
</header>

<nav class="tabs">
<button class="tab-btn active" data-tab="zles002">ZLES002</button>
<button class="tab-btn" data-tab="relatorio">Relatório</button>
<button class="tab-btn" data-tab="produtos-estrela">Produtos Estrela</button>
</nav>

<main>
<section id="zles002" class="tab-panel active">
<h2>Importação SAP (múltiplas linhas)</h2>
<div class="sub-card">
<label>
Cole as linhas do SAP (uma linha por item/remessa)
<textarea id="sap-linhas" rows="8" placeholder="Cole aqui várias linhas copiadas do SAP/Excel"></textarea>
</label>
<p class="hint">Formato esperado por linha (TAB): Cód Cliente, Cód Transportadora, Nº transporte, Inf Agenda, Nome Cliente, Qtde Remessa, Material, Setor, Cliente Pallet, Agrupamento Regional, Qtd Teórica Convertida, Nº NFe, Quantidade Teórica, Data Agendamento.</p>
<button id="importar-sap" type="button" class="primary">Importar linhas SAP</button>
<p id="status" aria-live="polite"></p>
</div>

<div class="sub-card">
<h3>DTs importadas no navegador</h3>
<table>
<thead>
<tr>
<th>DT</th>
<th>Cliente</th>
<th>Itens</th>
<th>Total fardos</th>
</tr>
</thead>
<tbody id="resumo-importacao"></tbody>
</table>
</div>
</section>

<section id="relatorio" class="tab-panel">
<h2>Relatório para Impressão</h2>
<div class="sub-card">
<label>Pesquisar DT (digite os 4 últimos números)
<input id="busca-dt" placeholder="Ex.: 4599" maxlength="20" />
</label>
<button id="pesquisar-dt" type="button">Pesquisar</button>
<select id="lista-dts"></select>
<button id="carregar-dt" type="button">Carregar DT selecionada</button>
<button id="imprimir" type="button" class="primary">Imprimir em paisagem</button>
</div>

<article id="print-area" class="print-sheet">
<header class="sheet-header">
<div>
<h3>Relatório de Paletização</h3>
<p id="sheet-dt">DT: -</p>
<p id="sheet-cliente">Cliente: -</p>
<p id="sheet-data">Data agendamento: -</p>
</div>
<div class="logo-wrap">
<img
src="https://upload.wikimedia.org/wikipedia/commons/thumb/0/0b/JSL_Logo.svg/320px-JSL_Logo.svg.png"
alt="Logo JSL"
onerror="this.style.display='none'; this.parentElement.innerHTML='<strong>JSL</strong>'"
/>
</div>
</header>

<table>
<thead>
<tr>
<th>Nº transporte</th>
<th>SKU</th>
<th>Qtde</th>
<th>Padrão Plt</th>
<th>Plt Fechado</th>
<th>Fração</th>
<th>Conferido</th>
<th>Avaria</th>
</tr>
</thead>
<tbody id="sheet-itens"></tbody>
<tfoot>
<tr>
<td colspan="2"><strong>Total de fardos</strong></td>
<td id="sheet-total">0</td>
<td colspan="5"></td>
</tr>
</tfoot>
</table>

<section class="observacoes">
<h4>Anotações do Conferente</h4>
<div class="notes-box"></div>
</section>

<p class="lacre">Lacre: _____________________________________________</p>
</article>
</section>

<section id="produtos-estrela" class="tab-panel">
<h2>Produtos Estrela</h2>
<table>
<thead>
<tr>
<th>SKU</th>
<th>Descrição</th>
</tr>
</thead>
<tbody id="produtos-estrela-body"></tbody>
</table>
</section>
</main>

<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
<script src="app.js"></script>
</body>
</html>
Loading