Controle de Produção Médica

DR. HERMES DA SILVA FEITOSA JUNIOR

Nuvem: aguardando conexão • Os dados ainda não foram sincronizados.

Painel financeiro

VALOR TOTAL
R$ 0,00
0 procedimentos
VALOR RECEBIDO
R$ 0,00
0 procedimentos pagos
PENDENTE DE PAGAMENTO
R$ 0,00
0 procedimentos pendentes
VALOR DE GLOSAS
R$ 0,00
Total registrado na planilha
Exibindo procedimentosSelecione “Editar” para atualizar o pagamento
Paciente / ConvênioDataGuia / AtendimentoProcedimentoPosiçãoValorGlosaPagamentoStatusAção
`; w.document.open();w.document.write(html);w.document.close(); } function exportCSV(){let cols=['Paciente','Convênio','Data','Guia','Atendimento','Código','Procedimento','Posição','Quantidade','Valor','Glosa','Status','Data pagamento','Origem'];let lines=[cols,...rows.map(r=>[r.patient,r.convenio,r.date,r.guide,r.attendance,r.code,r.description,r.position,r.qty,r.value,r.glosa,statusLabel(effectiveStatus(r)),r.paymentDate,r.source])];let csv='\ufeff'+lines.map(a=>a.map(v=>'"'+String(v??'').replace(/"/g,'""')+'"').join(';')).join('\r\n');let a=document.createElement('a');a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv;charset=utf-8'}));a.download='controle_producao_pagamentos.csv';a.click();URL.revokeObjectURL(a.href)} async function importFile(ev){let file=ev.target.files[0];if(!file)return;try{if(file.name.toLowerCase().endsWith('.csv')){let txt=await file.text();let parsed=parseCSV(txt);addImported(parsed)}else{if(!window.XLSX){alert('A biblioteca de leitura Excel não carregou. Conecte-se à internet e tente novamente, ou importe um CSV.');return}let buf=await file.arrayBuffer(),book=XLSX.read(buf,{type:'array',cellDates:true});let parsed=[];book.SheetNames.forEach(name=>{let a=XLSX.utils.sheet_to_json(book.Sheets[name],{defval:''});a.forEach(x=>parsed.push(normalizeImported(x,name))) });addImported(parsed)}}catch(e){alert('Não foi possível importar o arquivo: '+e.message)}ev.target.value=''} function normalizeImported(x,source){let o={};Object.keys(x).forEach(k=>o[k.toString().trim().toUpperCase()]=x[k]);let get=(...keys)=>{for(let k of keys)if(o[k]!==undefined&&o[k]!=='')return o[k];return ''};let dt=v=>{if(v instanceof Date)return v.toISOString().slice(0,10);if(typeof v==='number'&&v>20000){let d=new Date(Date.UTC(1899,11,30)+v*86400000);return d.toISOString().slice(0,10)}let s=String(v||'').trim();let m=s.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);return m?`${m[3]}-${m[2].padStart(2,'0')}-${m[1].padStart(2,'0')}`:s.slice(0,10)};let pg=dt(get('PGTO','DATA PGTO','DATA PAGAMENTO'));return {source,month:get('MÊS'),date:dt(get('DATA')),patient:String(get('PACIENTE')).trim(),convenio:get('CONVÊNIO'),guide:String(get('Nº GUIA','GUIA')),attendance:String(get('ATENDIMENTO')),demonstrative:String(get('DEMONST')),team:get('EQUIPE'),position:get('POSIÇÃO'),code:String(get('CÓDIGO')),description:get('DESCRIÇÃO'),qty:+get('QTD')||1,value:+String(get('VALOR')||0).replace(',','.'),urgency:+get('TX URGÊNCIA')||0,glosa:+get('GLOSA')||0,paymentDate:pg,status:pg?'paid':'pending'}} function parseCSV(txt){let lines=txt.replace(/^\uFEFF/,'').split(/\r?\n/).filter(Boolean),sep=lines[0].includes(';')?';':',';let parse=line=>{let out=[],cur='',q=false;for(let i=0;i{let a=parse(line),o={};h.forEach((k,i)=>o[k]=a[i]||'');return normalizeImported(o,'importado')})} function addImported(list){list=list.filter(r=>r.patient||r.description);if(!list.length){alert('Nenhum procedimento válido encontrado.');return}if(confirm(`Foram identificados ${list.length} procedimentos. Deseja adicionar à base atual?`)){rows=rows.concat(list);persist();initFilters();render();alert('Importação concluída.')}} function exportBackup(){const backup={app:'Controle de Produção Médica',version:1,createdAt:new Date().toISOString(),records:rows};const blob=new Blob([JSON.stringify(backup,null,2)],{type:'application/json;charset=utf-8'});const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='backup_controle_producao_'+new Date().toISOString().slice(0,10)+'.json';a.click();URL.revokeObjectURL(url)} function backupRecordKey(r){const guide=String(r.guide||r.attendance||'').trim();const patient=String(r.patient||'').trim().toLocaleLowerCase('pt-BR');const convenio=String(r.convenio||'').trim().toLocaleLowerCase('pt-BR');const service=String(r.code||'').trim()||String(r.description||'').trim().toLocaleLowerCase('pt-BR');if(guide)return [patient,convenio,guide,service].join('|');return [patient,convenio,String(r.date||''),String(r.demonstrative||''),service,String(r.value||0)].join('|')} async function importBackup(ev){const file=ev.target.files[0];if(!file)return;try{const parsed=JSON.parse(await file.text()),incoming=Array.isArray(parsed)?parsed:parsed.records;if(!Array.isArray(incoming))throw new Error('O arquivo não contém uma lista de registros válida.');if(!confirm('O backup contém '+incoming.length+' registros. Os procedimentos existentes serão atualizados, os novos serão adicionados e duplicatas por guia/serviço serão evitadas. Deseja continuar?'))return;const merged=new Map();for(const r of rows){const k=backupRecordKey(r);if(!merged.has(k))merged.set(k,r)}let updated=0,added=0;for(const raw of incoming){const r={...raw,demonstrative:raw.demonstrative||''};if((+r.glosa||0)>0)r.status='glosado';else if(!['paid','pending','glosado'].includes(r.status))r.status=r.paymentDate?'paid':'pending';const k=backupRecordKey(r);if(merged.has(k)){merged.set(k,{...merged.get(k),...r});updated++}else{merged.set(k,r);added++}}rows=[...merged.values()];currentPage=1;persist();initFilters();render();alert('Backup integrado com sucesso. '+updated+' procedimentos atualizados e '+added+' novos adicionados. Total: '+rows.length+'.')}catch(err){alert('Não foi possível importar o backup: '+err.message)}finally{ev.target.value=''}} function deleteAllData(){if(!rows.length){alert('Não há registros para excluir.');return}if(!confirm('ATENÇÃO: isso excluirá TODOS os '+rows.length+' registros deste navegador. Faça um backup antes de continuar. Deseja prosseguir?'))return;if(!confirm('Confirma definitivamente a exclusão de todos os dados?'))return;rows=[];persist();initFilters();render();alert('Todos os dados foram excluídos deste navegador.')} function excelDateToISO(v){ if(v instanceof Date)return v.toISOString().slice(0,10); let t=String(v??'').trim(),m=t.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); return m?`${m[3]}-${m[2].padStart(2,'0')}-${m[1].padStart(2,'0')}`:t.slice(0,10); } function moneyNum(v){ if(typeof v==='number')return v; let t=String(v??'').replace(/[R$\s]/g,'').trim();if(!t)return 0; if(t.includes(',')&&t.includes('.'))t=t.replace(/\./g,'').replace(',','.'); else if(t.includes(','))t=t.replace(',','.'); return Number(t)||0; } async function importUnimed(ev){ const file=ev.target.files && ev.target.files[0]; if(!file)return; try{ if(!window.XLSX) throw new Error('A biblioteca de leitura Excel não carregou. Atualize a página e tente novamente.'); const buffer=await file.arrayBuffer(); const workbook=XLSX.read(buffer,{type:'array',cellDates:true}); const sheet=workbook.Sheets[workbook.SheetNames[0]]; const matrix=XLSX.utils.sheet_to_json(sheet,{header:1,defval:'',raw:false}); const clean=v=>String(v??'').replace(/\s+/g,' ').trim(); const norm=v=>clean(v).normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLocaleLowerCase('pt-BR'); // Os rótulos de beneficiário e guia vêm com dois-pontos no Excel. // Aceita o rótulo exato ou o mesmo rótulo seguido de ':' sem confundir 'Cód' com 'Cód. Tab.'. const findCol=(row,label)=>row.findIndex(v=>{const n=norm(v),l=norm(label);return n===l||n.startsWith(l+':')}); const dateISO=v=>{ if(v instanceof Date&&!isNaN(v))return `${v.getFullYear()}-${String(v.getMonth()+1).padStart(2,'0')}-${String(v.getDate()).padStart(2,'0')}`; const t=clean(v); if(!t)return ''; let m=t.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/); if(m)return `${m[1]}-${m[2].padStart(2,'0')}-${m[3].padStart(2,'0')}`; m=t.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); if(m)return `${m[3]}-${m[2].padStart(2,'0')}-${m[1].padStart(2,'0')}`; return t; }; const money=v=>moneyNum(v); let operadora='',demonstrativo='',dataEmissao='',beneficiario='',guia=''; const imported=[]; let patientBlock=0; for(let i=0;i=0)operadora=clean((matrix[i+1]||[])[opCol])||operadora; const demoCol=findCol(row,'Nr. Demonstrativo'); if(demoCol>=0)demonstrativo=clean((matrix[i+1]||[])[demoCol])||demonstrativo; const emissionCol=findCol(row,'Data Emissão Demonstrativo'); if(emissionCol>=0)dataEmissao=dateISO((matrix[i+1]||[])[emissionCol])||dataEmissao; const patientCol=findCol(row,'Nome do Beneficiário'); if(patientCol>=0){beneficiario=clean(row[patientCol+1]);guia='';patientBlock++;} const guideCol=findCol(row,'Número da Guia'); if(guideCol>=0)guia=clean(row[guideCol+1]); const dateCol=findCol(row,'Data Realização'); const descCol=findCol(row,'Descrição Serviço'); const codeCol=findCol(row,'Cód'); const qtyCol=findCol(row,'Qtde Exec.'); const valueCol=findCol(row,'Vl Proces.'); const glosaCol=findCol(row,'Vl Glosa'); if(dateCol>=0&&descCol>=0&&codeCol>=0&&qtyCol>=0&&valueCol>=0&&glosaCol>=0){ for(let j=i+1;j0?'glosado':'paid',paymentDate:dataEmissao, groupId:'unimed_'+patientBlock+'_'+(guia||'semguia') }); } } } if(!imported.length)throw new Error('Nenhum serviço foi localizado. Confira o formato do Demonstrativo Analítico da Unimed.'); const missingPatient=imported.filter(x=>!x.patient).length; const missingDate=imported.filter(x=>!x.date).length; if(missingPatient||missingDate)throw new Error('Importação cancelada: '+missingPatient+' serviço(s) sem paciente e '+missingDate+' sem data de realização.'); if(!dataEmissao)throw new Error('Não encontrei a Data Emissão Demonstrativo. Nenhum registro foi importado.'); const patientCount=new Set(imported.map(x=>x.patient)).size; if(!confirm('Foram encontrados '+imported.length+' serviços de '+patientCount+' pacientes. Cada serviço será classificado pela glosa (acima de zero = GLOSADO; zero = PAGO), com data de pagamento '+dataEmissao+'. Demonstrativo '+(demonstrativo||'sem número')+'. Continuar?'))return; rows=rows.concat(imported);persist();initFilters();render(); alert('Importação concluída: '+imported.length+' serviços de '+patientCount+' pacientes. Status definido conforme o valor da glosa.'); }catch(error){alert('Falha ao importar Demonstrativo Unimed: '+(error&&error.message?error.message:error));} finally{ev.target.value='';} } async function importHab(ev){ const file=ev.target.files&&ev.target.files[0]; if(!file)return; try{ if(!/\.pdf$/i.test(file.name)&&file.type!=='application/pdf')throw new Error('Selecione um arquivo PDF do Demonstrativo HAB.'); if(!window.pdfjsLib)throw new Error('O leitor de PDF não carregou. Verifique sua conexão com a internet e tente novamente.'); pdfjsLib.GlobalWorkerOptions.workerSrc='https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; const pdf=await pdfjsLib.getDocument({data:await file.arrayBuffer()}).promise; const clean=v=>String(v??'').replace(/\s+/g,' ').trim(); const norm=v=>clean(v).normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLocaleLowerCase('pt-BR'); const dateISO=v=>{const t=clean(v);let m=t.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})$/);if(!m)return '';let y=m[3].length===2?'20'+m[3]:m[3];return `${y}-${m[2].padStart(2,'0')}-${m[1].padStart(2,'0')}`}; const money=v=>{let t=clean(v).replace(/[R$\s]/g,'');if(!t)return 0;if(t.includes(',')&&t.includes('.'))t=t.replace(/\./g,'').replace(',','.');else if(t.includes(','))t=t.replace(',','.');return Number(t)||0}; const imported=[];let currentRepasse='',currentPayDate='',currentDescription='',currentCode=''; for(let pageNo=1;pageNo<=pdf.numPages;pageNo++){ const page=await pdf.getPage(pageNo),content=await page.getTextContent(); // O relatório HAB/SoulMV é gerado com a página girada em 90 graus. // Transformamos as coordenadas para a orientação visual antes de agrupar as linhas. const viewport=page.getViewport({scale:1,rotation:page.rotate||0}),groups=[]; for(const item of content.items){ if(!item.str||!item.str.trim())continue; const tr=pdfjsLib.Util.transform(viewport.transform,item.transform),x=tr[4],y=tr[5]; let line=groups.find(g=>Math.abs(g.y-y)<2.5); if(!line){line={y,items:[]};groups.push(line)} line.items.push({x,text:clean(item.str)}); } const lines=groups.map(g=>({y:g.y,items:g.items.sort((a,b)=>a.x-b.x)})).sort((a,b)=>a.y-b.y); for(const line of lines){ const t=clean(line.items.map(z=>z.text).join(' ')),n=norm(t); if(/\brepasse\s*:/i.test(t)){ const m=t.match(/Repasse:\s*(\d+)/i);if(m)currentRepasse=m[1]; const d=t.match(/Dt\.?\s*Repasse:\s*(\d{1,2}\/\d{1,2}\/\d{2,4})/i);if(d)currentPayDate=dateISO(d[1]); } if(n.includes('procedimento:')){ const m=t.match(/Procedimento:\s*(\d{5,})\s*[-–]?\s*(.*)$/i); if(m){currentCode=m[1];currentDescription=clean(m[2])} } // Uma linha de atendimento do SoulMV começa por Remessa, Conta e Atend. // A coluna CP pode conter 'S' ou uma posição ordinal (ex.: '1º'). // A Atividade vem logo depois e o Convênio pode ter várias palavras. const row=t.match(/^(\d{5,7})\s+(\d{5,7})\s+(\d{6,8})\s+(.+?)\s+(\d{1,2}\/\d{1,2}\/\d{2,4})\s+([\d.,]+)\s+([\d.,]+)\s+([\d.,]+)\s+([\d.,]+)\s+([\d.,]+)(?:\s+.*)?$/i); if(!row)continue; const [,remessa,conta,attendance,peopleAndColumns,date,qty,glosaQty,glosaValue,qtch,repasseValue]=row; const fields=clean(peopleAndColumns).split(/\s+/); let cpIndex=-1; const activityNames=/^(CL[IÍ]NICO|AUXILIAR|CIRURG[IÍ]AO|CIRURGICO|ANESTESISTA|VISITADOR|PLANTONISTA)$/i; for(let i=1;i0||gq>0?'glosado':'paid'), groupId:'hab_'+(currentRepasse||'semrepasse')+'_'+clean(attendance)+'_'+dateISO(date) }); } } if(!imported.length)throw new Error('Nenhum atendimento foi localizado. O PDF foi lido, mas não foi possível reconhecer as linhas do relatório SoulMV. Verifique se o arquivo corresponde ao Demonstrativo de Repasse HAB.'); const unique=new Map();for(const r of imported){const k=[r.demonstrative,r.guide,r.date,r.code].join('|');if(!unique.has(k))unique.set(k,r)} const list=[...unique.values()],missingCode=list.filter(x=>!x.code).length,missingDescription=list.filter(x=>x.description==='Procedimento não identificado no PDF').length; const patientCount=new Set(list.map(x=>x.patient)).size; if(!confirm('Foram encontrados '+list.length+' atendimentos de '+patientCount+' pacientes no Demonstrativo HAB. Nº Guia receberá o número do atendimento; a data de pagamento será a data do repasse. Os valores serão importados conforme o relatório SoulMV.'+(missingCode?' '+missingCode+' registro(s) sem código de procedimento.':'')+(missingDescription?' '+missingDescription+' registro(s) sem descrição de procedimento.':'')+' Registros com glosa serão classificados como GLOSADO e os demais como PAGO. Deseja continuar?'))return; rows=rows.concat(list);persist();initFilters();render();alert('Importação HAB concluída: '+list.length+' atendimentos de '+patientCount+' pacientes importados.'); }catch(error){alert('Falha ao importar Demonstrativo HAB: '+(error&&error.message?error.message:error));} finally{ev.target.value='';} } initFilters();render();if(cloudSecret){connectCloud(false)}else{cloudStatus('Nuvem: aguardando link privado','Abra o endereço com a chave privada para conectar este dispositivo')} document.addEventListener('click',e=>{const box=document.getElementById('convFilter');if(box&&!box.contains(e.target))box.classList.remove('open')});