PROCESSANDO...
Peças desta Carga
0 peças

Acabamentos

🗓 Ano
📅 Mês
📍 Período início
📍 Período fim
🏢 Obras (CT)
Todas as obras
📦 Produto
Todos
🧱 Concretagem
📄 Romaneio
🏭 Unidade
Carregando dados do Planejamento Tático…
Peças no período
Média / dia útil
Obras em acabamento
Dia de pico
Peças por dia · por obra
sem acabamento · do que está em carga
Mapa de peças · obra × dia
sem acabamento · do que está em carga · intensidade = carga de trabalho do dia
Peças por dia · por produto · por obra
sem acabamento · do que está em carga · clique no ▸ para abrir as obras do produto
Peças por dia · por vão · por obra
sem acabamento · do que está em carga · vão da Região de Estoque (ou da Localização de Fabricação) · em ordem alfabética
Tipos de consolo
usados no lançamento por peça e na medição do acabamento · o preço de cada um é cadastrado no módulo Medições
Peças · consolos
carregando…
Peça Obra Etapa Unidade Produto Consolos
Acabamentos · v1.54
Gestão da Qualidade · Acabamentos
Entre com seu e-mail e senha
'; } function ligarRomaneio(){ var b=document.getElementById('flt-pdf-btn'); if(b) b.addEventListener('click', function(){ if(unidadeDoRomaneio()) abrirModalRomaneio(); }); var x=document.getElementById('rom-md-x'); if(x) x.addEventListener('click', fecharModalRomaneio); var c=document.getElementById('rom-md-cancelar'); if(c) c.addEventListener('click', fecharModalRomaneio); var g=document.getElementById('rom-md-gerar'); if(g) g.addEventListener('click', gerarRomaneio); var ov=document.getElementById('rom-modal'); if(ov) ov.addEventListener('click', function(ev){ if(ev.target===ov) fecharModalRomaneio(); }); document.addEventListener('keydown', function(ev){ if(ev.key==='Escape') fecharModalRomaneio(); }); sincronizarBotaoRomaneio(); } /* Toggles Peças / Volume dos dois gráficos. */ function ligarToggles(){ // Um seletor por seção: cada um mexe só no próprio gráfico e na própria // tabela. O id carrega a seção ('pg-met-obra' -> 'obra'). ['obra','heat','prod','vao'].forEach(function(qual){ var box=document.getElementById('pg-met-'+qual); if(!box) return; box.addEventListener('click',function(ev){ var b=ev.target.closest('button[data-m]'); if(!b) return; box.querySelectorAll('button').forEach(function(x){ x.classList.toggle('on', x===b); }); PG_MET[qual]=b.dataset.m; keepScroll(function(){ renderProg(); }); }); }); [['vg-met-12m',function(v){VG_MET_12M=v;}],['vg-met-spint',function(v){VG_MET_SPINT=v;}], ['vg-met-equipe',function(v){VG_MET_EQUIPE=v;}]].forEach(function(p){ var box=document.getElementById(p[0]); if(!box) return; box.addEventListener('click',function(ev){ var b=ev.target.closest('button[data-m]'); if(!b) return; box.querySelectorAll('button').forEach(function(x){ x.classList.toggle('on', x===b); }); p[1](b.dataset.m); keepScroll(function(){ renderVisao(); }); }); }); } // ── TIPOS DE CONSOLO (tabela pcp_consolo_tipos) ──────────────── // Lista viva: o modulo nunca conhece "tipo 1, 2 e 3" — quem define e o // cadastro. Isso e o que permite a mesma tela servir a outra empresa. var CONSOLO_TIPOS = []; // [{id, nome, ativo}] async function carregarTiposConsolo(){ if(!SB_ACAB) return; var r = await SB_ACAB.from('pcp_consolo_tipos').select('id,nome,ativo,unidade').order('id'); if(r.error){ console.warn('[PCP] tipos de consolo', r.error); var box = document.getElementById('tc-lista'); if(box) box.innerHTML = '
' + 'Não foi possível ler os tipos de consolo: ' + escHtml(r.error.message) + '
'; return; } CONSOLO_TIPOS = r.data || []; renderTiposConsolo(); renderPecasConsolo(); } /* A unidade só é uma quando o filtro tem exatamente uma escolhida. Sem isso não dá para saber em qual fábrica cadastrar o tipo — e "cadastrar em todas" seria pior que não deixar cadastrar. */ function unidadeAtivaUnica(){ var us = unidadesAtivas(); return us.length===1 ? us[0] : null; } /* Tipos de uma unidade. O legado sem unidade (linhas anteriores à migração) é tratado como CAPITAL, que é onde eles foram criados. */ function tiposDaUnidade(u){ return CONSOLO_TIPOS.filter(function(t){ return (t.unidade || 'CAPITAL') === u; }); } // O codigo e o nome do tipo sao texto digitado por qualquer usuario com o // modulo Acabamento, salvos na nuvem e mostrados para todo mundo. Sem escapar, // um Codigo com apostrofo fecha a string do onclick e injeta JavaScript no // navegador do proximo colega que abrir a aba: XSS persistente. Por isso o // escape, e por isso o botao usa data-id + delegacao em vez de montar o // argumento dentro do atributo. function escHtml(v){ return String(v == null ? '' : v).replace(/[&<>"']/g, function(c){ return { '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c]; }); } function renderTiposConsolo(){ var el = document.getElementById('tc-lista'); if(!el) return; var u = unidadeAtivaUnica(); var tit = document.getElementById('tc-titulo'); var form = document.getElementById('tc-form'); var st = document.getElementById('tc-status'); // Sem uma única unidade não há onde cadastrar: o formulário some, e a lista // dá lugar ao aviso. Esconder o formulário é o que impede o tipo de nascer // na fábrica errada. if(!u){ if(tit) tit.textContent = 'Tipos de consolo'; if(form) form.style.display = 'none'; if(st) st.style.display = 'none'; el.innerHTML = '
Escolha uma única unidade no filtro acima para ver e cadastrar os tipos de consolo dela.
'; return; } if(tit) tit.textContent = 'Tipos de consolo · ' + u; if(form) form.style.display = ''; var lista = tiposDaUnidade(u); if(!lista.length){ el.innerHTML = '
' + 'Nenhum tipo cadastrado em ' + escHtml(u) + ' ainda. Sem tipo, o consolo não pode ser lançado nem medido.
'; return; } el.innerHTML = '' + '' + '' + '' + lista.map(function(t){ return '' + '' + ''; }).join('') + '
CódigoNomeAtivo
'+escHtml(t.id)+''+escHtml(t.nome)+'' + '' + '
'; // Delegacao: a tabela e redesenhada inteira a cada carga, entao ouvir no // container sobrevive ao redesenho sem precisar religar botao por botao. el.onclick = function(ev){ var b = ev.target.closest('.tc-toggle'); if(b) alternarTipoConsolo(b.getAttribute('data-id')); }; } async function salvarTipoConsolo(){ var id = (document.getElementById('tc-id').value||'').trim().toUpperCase(); var nome = (document.getElementById('tc-nome').value||'').trim(); var s = document.getElementById('tc-status'); var aviso = function(msg, ok){ s.textContent=msg; s.className='manual-status '+(ok?'ok':'err'); s.style.display='block'; }; if(!id || !nome){ aviso('⚠ Informe o código e o nome do tipo.', false); return; } var u = unidadeAtivaUnica(); if(!u){ aviso('⚠ Escolha uma única unidade no filtro acima antes de cadastrar.', false); return; } // O código do tipo é a CHAVE PRIMÁRIA e é global, não por unidade. Sem esta // checagem, cadastrar "T1" em INTERIOR faria um upsert por cima do "T1" de SP: // o tipo mudaria de fábrica em silêncio, levando junto todo consolo já // lançado nele — inclusive o de medição fechada. var jaExiste = CONSOLO_TIPOS.filter(function(t){ return t.id===id; })[0]; if(jaExiste && (jaExiste.unidade || 'CAPITAL') !== u){ aviso('⚠ O código ' + id + ' já existe em ' + (jaExiste.unidade || 'CAPITAL') + ' ("' + jaExiste.nome + '"). O código é único no sistema inteiro — ' + 'use outro para ' + u + '.', false); return; } var r = await SB_ACAB.from('pcp_consolo_tipos') .upsert({ id:id, nome:nome, ativo:true, unidade:u, updated_at:new Date().toISOString() }, { onConflict:'id' }); if(r.error){ aviso('⚠ Não foi possível salvar: '+r.error.message, false); return; } document.getElementById('tc-id').value=''; document.getElementById('tc-nome').value=''; aviso('✓ Tipo '+id+' salvo.', true); await carregarTiposConsolo(); } // Tipo nunca e apagado: ele pode estar dentro de uma medicao ja fechada, e // apagar quebraria a referencia do que foi pago. Desativar tira das listas // novas sem mexer no passado. async function alternarTipoConsolo(id){ var t = CONSOLO_TIPOS.filter(function(x){return x.id===id;})[0]; if(!t) return; var r = await SB_ACAB.from('pcp_consolo_tipos') .update({ ativo: !t.ativo, updated_at:new Date().toISOString() }).eq('id', id); if(r.error){ console.warn('[PCP] alternar tipo', r.error); return; } await carregarTiposConsolo(); } // ── CONSOLOS POR PECA (tabela pcp_consolos) ──────────────────── // Formato identico ao que o PCPMedCore.montarLinhas espera em opcoes.consolos. var CONSOLOS_POR_PECA = {}; // { codigo: { tipoId: quantidade } } // Pagina em blocos de 1000, o mesmo padrao do montagem.html: o PostgREST tem // teto default por requisicao e trunca EM SILENCIO. Aqui truncar significa // consolo lancado que some da tela e da medicao — some do valor a pagar. // A falha de leitura tambem nao pode ser muda: uma lista vazia por erro de rede // e indistinguivel de "nao ha consolo nenhum", e essa e a leitura errada. async function carregarConsolosPorPeca(){ if(!SB_ACAB) return; var mapa = {}, PAGE = 1000, from = 0; while(true){ var r = await SB_ACAB.from('pcp_consolos').select('codigo,tipo_id,quantidade') .range(from, from + PAGE - 1); if(r.error){ console.warn('[PCP] consolos por peca', r.error); var s = document.getElementById('cp-status'); if(s){ s.textContent = '⚠ Não foi possível ler os consolos já lançados: ' + r.error.message + '. A tela pode estar mostrando menos do que existe.'; s.className = 'manual-status err'; s.style.display = 'block'; } return; // mantem o mapa anterior: melhor dado velho do que zero falso } var lote = r.data || []; lote.forEach(function(x){ if(!mapa[x.codigo]) mapa[x.codigo] = {}; mapa[x.codigo][x.tipo_id] = x.quantidade; }); if(lote.length < PAGE) break; from += PAGE; } CONSOLOS_POR_PECA = mapa; } /* ═══════════ CONSOLES · lista peça a peça (v1.25) ═══════════ Antes daqui existia um lançamento EM LOTE (filtra obra/produto/seção e aplica a mesma quantidade a todas). Saiu a pedido: o consolo varia peça a peça, e um lote errado escrevia em centenas de peças de uma vez. Contrato com o Medições, inalterado: pcp_consolos tem chave (codigo,tipo_id) e a AUSÊNCIA de linha significa zero. Por isso quantidade 0 apaga a linha em vez de gravar 0 — gravar 0 criaria linha que o Medições leria como lançada. */ var CP_BUSCA = ''; var CP_EDIT = null; // { codigo, peca } da peça aberta no modal /* Peças da aba, no mesmo recorte da toolbar das outras abas. */ function getPecasConsoloFiltradas(){ var us=unidadesAtivas(), cts=obrasAtivasSet(), prods=produtosAtivosSet(); var soConcretadas = !LF || LF.concret !== 'todas'; var q = CP_BUSCA.trim().toLowerCase(); return PECAS_CONSOLO.filter(function(p){ if(!ehPecaDoAcabamento(p)) return false; if(us.length && us.indexOf(p.unidade)===-1) return false; if(cts && !cts.has(p.sigla)) return false; if(prods && !prods.has(p.produto)) return false; if(soConcretadas && !p.dataCO) return false; if(q && (p.peca+' '+p.codigo+' '+p.obra+' '+p.produto+' '+p.etapa).toLowerCase().indexOf(q)===-1) return false; return true; }); } function totalConsolosDa(codigo){ var m = CONSOLOS_POR_PECA[codigo]; if(!m) return 0; return Object.keys(m).reduce(function(s,k){ return s + (Number(m[k])||0); }, 0); } /* Teto de linhas desenhadas. Sem ele, "Todas as obras" monta uma tabela de dezenas de milhares de e a aba trava ao abrir. O rodapé diz quantas ficaram de fora, para o número nunca mentir. */ var CP_TETO = 500; function renderPecasConsolo(){ var body=document.getElementById('cp-body'); if(!body) return; var lista = getPecasConsoloFiltradas(); var sub=document.getElementById('cp-sub'); var mostradas = lista.slice(0, CP_TETO); if(sub){ var comConsolo = lista.filter(function(p){ return totalConsolosDa(p.codigo)>0; }).length; sub.textContent = lista.length ? (nf0(lista.length)+' peça(s) no recorte · '+nf0(comConsolo)+' com consolo lançado' + (lista.length>CP_TETO ? ' · mostrando as '+CP_TETO+' primeiras' : '')) : 'nenhuma peça no recorte dos filtros'; } if(!mostradas.length){ body.innerHTML = 'Nenhuma peça no recorte dos filtros.'; return; } body.innerHTML = mostradas.map(function(p){ var t = totalConsolosDa(p.codigo); return '' + ''+escHtml(p.peca||p.codigo)+'' + ''+escHtml(p.obra||'—')+'' + ''+escHtml(p.etapa||'—')+'' + ''+badgeUnidade(p.unidade)+'' + ''+escHtml(p.produto||'—')+'' + ''+(t?nf0(t):'·')+'' + '' + ''; }).join(''); // Delegação: a tabela é redesenhada inteira a cada filtro. body.onclick = function(ev){ var b = ev.target.closest && ev.target.closest('.cp-edit'); if(b) abrirModalConsolo(b.getAttribute('data-cod')); }; } /* ── Modal de quantidades ── Um campo por tipo ATIVO da unidade DA PEÇA — não da unidade do filtro: a peça é de onde é, e oferecer o tipo de outra fábrica geraria lançamento que o Medições não sabe precificar. */ async function abrirModalConsolo(codigo){ var p = PECAS_CONSOLO.filter(function(x){ return x.codigo===codigo; })[0]; if(!p) return; var atuais = CONSOLOS_POR_PECA[codigo] || {}; var daUnidade = tiposDaUnidade(p.unidade).filter(function(t){ return t.ativo; }); // Tipo de FORA da unidade que já tem quantidade lançada nesta peça entra // mesmo assim. Escondê-lo deixaria um número invisível somando no total da // lista — e sem campo, não haveria como zerá-lo. var forasteiros = CONSOLO_TIPOS.filter(function(t){ return (Number(atuais[t.id])||0) > 0 && daUnidade.indexOf(t)===-1; }); var tipos = daUnidade.concat(forasteiros); var campos=document.getElementById('cp-md-campos'); var msg=document.getElementById('cp-md-msg'); msg.textContent=''; msg.className='manual-status'; msg.style.display='none'; if(!tipos.length){ campos.innerHTML = '
Nenhum tipo de consolo ativo cadastrado para ' + escHtml(p.unidade||'esta unidade') + '. Cadastre acima antes de lançar.
'; document.getElementById('cp-md-salvar').disabled = true; } else { campos.innerHTML = tipos.map(function(t){ var v = Number(atuais[t.id]) || 0; var fora = daUnidade.indexOf(t)===-1; return '
' + '
'; }).join(''); document.getElementById('cp-md-salvar').disabled = false; } CP_EDIT = { codigo: codigo, peca: p.peca || codigo }; document.getElementById('cp-md-tit').textContent = 'Consolos — peça ' + (p.peca||codigo); document.getElementById('cp-md-sub').textContent = (p.obra||'') + ' · ' + (p.produto||'') + ' · ' + (p.unidade||''); document.getElementById('cp-modal').classList.add('on'); /* Trava de medição fechada: avisa ANTES de digitar, não depois de gravar. Via RPC porque a política de leitura de pcp_medicoes_itens exige o módulo 'medicoes', que o usuário do Acabamento não tem — a consulta direta voltaria vazia, sem erro, dizendo "nada travado". */ if(!SB_ACAB) return; var r = await SB_ACAB.rpc('pcp_pecas_travadas', { codigos:[codigo], et:'acabamento' }); if(!CP_EDIT || CP_EDIT.codigo !== codigo) return; // fechou ou trocou de peça if(r.error){ msg.textContent = '⚠ Não foi possível checar se esta peça está em medição fechada (' + r.error.message + '). Se estiver, a gravação será recusada.'; msg.className='manual-status err'; msg.style.display='block'; return; } if((r.data||[]).length){ msg.textContent = '⚠ Esta peça está em medição de acabamento já fechada. Para alterar, ' + 'um admin precisa reabrir a medição.'; msg.className='manual-status err'; msg.style.display='block'; document.getElementById('cp-md-salvar').disabled = true; } } function fecharModalConsolo(){ document.getElementById('cp-modal').classList.remove('on'); CP_EDIT = null; } async function salvarModalConsolo(){ var ed = CP_EDIT; if(!ed || !SB_ACAB) return; var msg=document.getElementById('cp-md-msg'); var btn=document.getElementById('cp-md-salvar'); var aviso=function(t,ok){ msg.textContent=t; msg.className='manual-status '+(ok?'ok':'err'); msg.style.display='block'; }; var atuais = CONSOLOS_POR_PECA[ed.codigo] || {}; var gravar=[], apagar=[]; Array.prototype.forEach.call(document.querySelectorAll('#cp-md-campos .cp-qtd'), function(inp){ var tipo = inp.getAttribute('data-tipo'); var nova = Math.max(0, Math.trunc(Number(inp.value)||0)); var antes = Number(atuais[tipo])||0; if(nova===antes) return; // só grava o que mudou if(nova>0) gravar.push({ tipo_id:tipo, quantidade:nova }); else apagar.push(tipo); }); if(!gravar.length && !apagar.length){ fecharModalConsolo(); return; } btn.disabled = true; var uid=null; try{ uid=(await SB_ACAB.auth.getUser()).data.user?.id||null; }catch(e){} var agora=new Date().toISOString(); /* upsert e delete são DUAS chamadas sem transação entre elas. Cada uma aplica o PRÓPRIO sucesso no cache assim que volta: se a segunda falhar, o que está na tela continua sendo o que está no banco. */ var erroG=null, erroA=null; if(gravar.length){ var linhas = gravar.map(function(m){ return { codigo:ed.codigo, tipo_id:m.tipo_id, quantidade:m.quantidade, updated_at:agora, updated_by:uid }; }); var r1 = await SB_ACAB.from('pcp_consolos').upsert(linhas, { onConflict:'codigo,tipo_id' }); if(r1.error) erroG = r1.error; else { if(!CONSOLOS_POR_PECA[ed.codigo]) CONSOLOS_POR_PECA[ed.codigo] = {}; linhas.forEach(function(l){ CONSOLOS_POR_PECA[ed.codigo][l.tipo_id] = l.quantidade; }); } } if(apagar.length){ var r2 = await SB_ACAB.from('pcp_consolos').delete().eq('codigo', ed.codigo).in('tipo_id', apagar); if(r2.error) erroA = r2.error; else if(CONSOLOS_POR_PECA[ed.codigo]){ apagar.forEach(function(t){ delete CONSOLOS_POR_PECA[ed.codigo][t]; }); } } btn.disabled = false; keepScroll(function(){ renderPecasConsolo(); }); if(!erroG && !erroA){ fecharModalConsolo(); return; } console.warn('[PCP] gravar consolo', erroG, erroA); // A RLS recusa peça em medição fechada. Não é erro de sistema: é a trava // funcionando, e precisa ser dita com essas palavras. var parcial = (gravar.length && !erroG) || (apagar.length && !erroA); var e = erroG || erroA; aviso((parcial ? 'Só parte da alteração foi gravada. ' : 'Não foi possível gravar. ') + 'Se esta peça estiver em medição de acabamento já fechada, ela está travada — ' + 'um admin precisa reabrir a medição. (' + (e && e.message ? e.message : 'erro desconhecido') + ')', false); } function ligarConsoles(){ var b=document.getElementById('cp-busca'); if(b) b.addEventListener('input', function(ev){ CP_BUSCA = ev.target.value; keepScroll(function(){ renderPecasConsolo(); }); }); var x=document.getElementById('cp-md-x'); if(x) x.addEventListener('click', fecharModalConsolo); var c=document.getElementById('cp-md-cancelar'); if(c) c.addEventListener('click', fecharModalConsolo); var s=document.getElementById('cp-md-salvar'); if(s) s.addEventListener('click', salvarModalConsolo); var ov=document.getElementById('cp-modal'); if(ov) ov.addEventListener('click', function(ev){ if(ev.target===ov) fecharModalConsolo(); }); document.addEventListener('keydown', function(ev){ if(ev.key==='Escape') fecharModalConsolo(); }); } // ── INIT ────────────────────────────────────────────────────── updateDate(); // Sem auto-load aqui: o gate de login chama window.pcpIniciarAcabamento(SB), // que carrega os Consolos e o dataset do Tático da nuvem.