-
Notifications
You must be signed in to change notification settings - Fork 0
Add client-side SAP import UI and printable DT paletization report #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This row template injects 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(); | ||
| 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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎.