Cole a URL do seu Apps Script. Feito uma vez — fica salvo no navegador.
1URL do Apps Script
Não configurou ainda?
1. Google Sheets → Extensões → Apps Script
2. Cole o codigo_apps_script.gs → Salve
3. Selecione inicializar → clique ▶️
4. Implantar → Nova implantação
5. Tipo: Aplicativo da Web / Acesso: Qualquer pessoa
6. Copie a URL e cole acima
🔧

MSJ Software de Gestão para Oficinas

Faça login para continuar

`; // Em vez de document.write (instável em navegadores in-app como WhatsApp/Instagram // e sujeito a "tela preta" com modo escuro forçado), navega a janela para um Blob real. const blob = new Blob([html], {type:'text/html'}); const blobUrl = URL.createObjectURL(blob); w.location.href = blobUrl; setTimeout(()=>URL.revokeObjectURL(blobUrl), 120000); } // ===================================================== // ORDENS DE SERVIÇO — com status // ===================================================== let osI=[]; function navOSMes(d){osMes+=d;if(osMes>12){osMes=1;osAno++;}if(osMes<1){osMes=12;osAno--;}loadOS(osBuscaAtual);} function hojeOSMes(){const h=new Date();osMes=h.getMonth()+1;osAno=h.getFullYear();loadOS(osBuscaAtual);} let osBuscaAtual=''; function osConcluida(o){return (o.etapa||'diagnostico')==='retirada';} async function loadOS(s='',mes=osMes,ano=osAno){ osBuscaAtual=s;osMes=mes;osAno=ano; setSS('s','...'); try{ const{lista}=await apiGet({acao:'listar',tabela:'ordens'}); // OS concluída fica fixa no mês em que foi retirada (data_conclusao, // com fallback pra data de abertura em OS antigas sem esse campo). // OS ainda não concluída "acompanha" o mês atual real, até ser // finalizada — por isso só aparece quando o mês visto é o de hoje. const doMes=lista.filter(o=>{ if(osConcluida(o)){ const ma=mesAnoDe(o.data_conclusao)||mesAnoDe(o.data); return ma && ma.mes===mes && ma.ano===ano; } return ehMesAtualReal(mes,ano); }); const filtrado=s?doMes.filter(o=>o.cliente.toLowerCase().includes(s.toLowerCase())||o.veiculo.toLowerCase().includes(s.toLowerCase())):doMes; // Pendentes/abertas aparecem primeiro (precisam de atenção); pagas vão // pro final da lista, mantendo a ordem cronológica dentro de cada grupo. const ordenado=filtrado.slice().sort((a,b)=>{ const pa=(a.status==='pago')?1:0, pb=(b.status==='pago')?1:0; return pa-pb; }); setSS('ok','OK'); el('content').innerHTML=` ${periodNavHtml(mes,ano,'navOSMes','hojeOSMes')}
${ordenado.length?ordenado.map(o=>``).join(''):``}
#ClienteVeículoDataCobradoLucroEtapaStatus
${o.num} ${o.cliente} ${o.veiculo} ${fmtD(o.data)} ${fmt(o.valor)} ${fmt(Number(o.valor)-Number(o.custo))} ${badgeEtapa(o.etapa)}${osAtrasada(o)?' Atrasada':''} ${badgeOS(o.status)}
${isDono()?``:''}
Nenhuma OS neste mês
`; }catch(e){setSS('e','Erro');el('content').innerHTML=`
⚠️ ${e.message}
`;} } function alterarStatusOS(id, statusAtual){ showM(`

Selecione o novo status:

✅ O status Pago é marcado sozinho quando você quitar a cobrança dessa OS em Contas a Receber.

`); } async function setStatusOS(id, status){ setSS('s','Salvando...'); const r=await apiPost({acao:'atualizar',tabela:'ordens',id,dados:{status}}); if(r.erro){setSS('e','Erro');alert(r.erro);return;} setSS('ok','Atualizado');cM();loadOS(osBuscaAtual); } async function oOSModal(){ osI=[]; const{lista}=await apiGet({acao:'listar',tabela:'estoque'}); const comEst=lista.filter(p=>Number(p.qtd)>0); const hoje=new Date().toISOString().split('T')[0]; showM(`
Peças Utilizadas
Custo
${fmt(0)}
Cobrado
${fmt(0)}
Lucro
${fmt(0)}
`); rOIL(); } function setModoPecaOS(m){ el('os-peca-estoque').style.display=m==='estoque'?'grid':'none'; el('os-peca-externa').style.display=m==='externa'?'grid':'none'; el('os-ext-hint').style.display=m==='externa'?'block':'none'; el('os-modo-estoque').className='btn btn-xs'+(m==='estoque'?' btn-p':''); el('os-modo-externa').className='btn btn-xs'+(m==='externa'?' btn-p':''); } function addOI(){const sel=el('os-p');if(!sel||!sel.value)return;const opt=sel.options[sel.selectedIndex];const pid=sel.value,qtd=parseInt(gv('os-q'))||1;const nome=opt.dataset.nome,custo=parseFloat(opt.dataset.custo)||0,preco_venda=parseFloat(opt.dataset.venda)||custo,maxQ=parseInt(opt.dataset.qtd)||0;const ja=osI.find(i=>i.pid===pid);const tj=ja?ja.qtd:0;if(tj+qtd>maxQ){alert('Estoque insuficiente. Disponível: '+(maxQ-tj));return;}if(ja)ja.qtd+=qtd;else osI.push({pid,nome,qtd,custo,preco_venda,externa:false});rOIL();calcOS();} function addOIExterna(){ const nome=gv('os-ext-nome').trim(),custo=parseFloat(gv('os-ext-custo'))||0,preco_venda=parseFloat(gv('os-ext-venda'))||custo,qtd=parseInt(gv('os-ext-qtd'))||1; if(!nome){alert('Digite o nome da peça.');return;} osI.push({pid:null,nome,qtd,custo,preco_venda,externa:true}); el('os-ext-nome').value='';el('os-ext-custo').value='';el('os-ext-venda').value='';el('os-ext-qtd').value='1'; rOIL();calcOS(); } function rmOI(idx){osI.splice(idx,1);rOIL();calcOS();} function rOIL(){const e=el('os-il');if(!e)return;if(!osI.length){e.innerHTML='
Nenhuma peça adicionada
';return;}e.innerHTML=`
${osI.map((i,idx)=>`
${i.externa?'🛒 ':''}${i.nome}×${i.qtd}${fmt(i.qtd*i.preco_venda)}
`).join('')}
`;} function calcOS(){ const c=osI.reduce((s,i)=>s+i.qtd*i.custo,0); const sugestao=osI.reduce((s,i)=>s+i.qtd*i.preco_venda,0); const valInp=el('os-val'); if(valInp&&!valInp.value&&sugestao)valInp.value=sugestao.toFixed(2); const val=parseFloat(gv('os-val'))||0,l=val-c; const cd=el('os-cd');if(cd)cd.textContent=fmt(c); const vd=el('os-vd');if(vd)vd.textContent=fmt(val); const ld=el('os-ld');if(ld){ld.textContent=fmt(l);ld.style.color=l>=0?'var(--grn)':'var(--red)';}; } async function saveOS(){ const cliente=gv('os-c'),veiculo=gv('os-v'),data=gv('os-d'),prazo_entrega=gv('os-prazo'),valor=parseFloat(gv('os-val'))||0,status=gv('os-st')||'aberta'; if(!cliente||!veiculo){alert('Cliente e veículo obrigatórios.');return;} const custo=osI.reduce((s,i)=>s+i.qtd*i.custo,0); setSS('s','Salvando...'); const{lista}=await apiGet({acao:'listar',tabela:'ordens'}); const num='OS-'+String((lista.length||0)+1).padStart(4,'0'); const res=await apiPost({acao:'inserir',tabela:'ordens',dados:{num,cliente,veiculo,data,prazo_entrega,valor,custo,status,etapa:'diagnostico'}}); for(const item of osI){ await apiPost({acao:'inserir',tabela:'os_itens',dados:{ordem_id:res.id,produto_id:item.pid,nome:item.nome,qtd:item.qtd,custo:item.custo,preco_venda:item.preco_venda}}); const{lista:el2}=await apiGet({acao:'listar',tabela:'estoque'}); const prod=el2.find(p=>p.id===item.pid); if(prod)await apiPost({acao:'atualizar',tabela:'estoque',id:item.pid,dados:{qtd:Number(prod.qtd)-item.qtd}}); } setSS('ok','Salvo');cM();loadOS(osBuscaAtual); } async function vOS(id){ const{lista:itens}=await apiGet({acao:'listar',tabela:'os_itens',ordem_id:id}); const{lista:ordens}=await apiGet({acao:'listar',tabela:'ordens'}); const o=ordens.find(x=>x.id===id);if(!o)return; const lucro=Number(o.valor)-Number(o.custo); const etapaAtual=o.etapa||'diagnostico'; const idxAtual=ETAPAS_OS.findIndex(e=>e.id===etapaAtual); showM(`
Cliente
${o.cliente}
Veículo
${o.veiculo}
Data
${fmtD(o.data)}
Status
${badgeOS(o.status)}
Etapa do atendimento ${osAtrasada(o)?'— ATRASADA':''}
${ETAPAS_OS.map((e,i)=>``).join('')}
${itens.length?itens.map(i=>`
${i.nome}×${i.qtd}${fmt(i.qtd*Number(i.preco_venda||i.custo))}
`).join(''):'
Nenhuma peça
'}
Custo
${fmt(o.custo)}
Cobrado
${fmt(o.valor)}
Lucro
${fmt(lucro)}
`); } async function setEtapaOS(id,etapa){ setSS('s','Salvando...'); // Ao concluir (retirada), grava a data de conclusão — é ela que fixa // em qual mês essa OS vai aparecer dali pra frente. const dados=etapa==='retirada'?{etapa,data_conclusao:new Date().toISOString().split('T')[0]}:{etapa}; await apiPost({acao:'atualizar',tabela:'ordens',id,dados}); setSS('ok','Atualizado'); if(etapa==='retirada'){ cM(); mostrarAvaliacaoOS(id); }else{ vOS(id); } loadOS(osBuscaAtual); } async function mostrarAvaliacaoOS(id){ const link=CFG.link_avaliacao_google||''; const{lista:ordens}=await apiGet({acao:'listar',tabela:'ordens'}); const o=ordens.find(x=>x.id===id); const nomeCliente=o?o.cliente:''; const nomeOficina=CFG.nome_oficina||'nossa oficina'; const msg=`Olá, ${nomeCliente}! Seu veículo já está pronto para retirada na ${nomeOficina}. 🚗✅${link?`\n\nSe puder, deixe sua avaliação sobre o atendimento, é rapidinho:\n${link}`:''}`; showM(`

A etapa foi marcada como Retirada. ${link?'Copie a mensagem abaixo e envie pro cliente pedindo a avaliação:':'Cadastre o link de avaliação do Google nas configurações pra poder enviar isso junto.'}

${link?`
${msg}
`:''} `); } async function delOS(id){if(!confirm('Remover esta OS?\n\n✅ As peças serão devolvidas ao estoque automaticamente.'))return;setSS('s','Removendo...');await apiPost({acao:'deletar',tabela:'ordens',id});setSS('ok','Removido — estoque restaurado');loadOS(osBuscaAtual);} // ===================================================== // CONTAS A RECEBER // ===================================================== function navReceberMes(d){receberMes+=d;if(receberMes>12){receberMes=1;receberAno++;}if(receberMes<1){receberMes=12;receberAno--;}loadReceber(receberFilAtual);} function hojeReceberMes(){const h=new Date();receberMes=h.getMonth()+1;receberAno=h.getFullYear();loadReceber(receberFilAtual);} let receberFilAtual='todos'; async function loadReceber(fil='todos',mes=receberMes,ano=receberAno){ receberFilAtual=fil;receberMes=mes;receberAno=ano; setSS('s','...'); try{ const res=await apiGet({acao:'listar',tabela:'receber'}); if(res.erro){ setSS('e','Erro'); el('content').innerHTML=`
⚠️ ${res.erro}
📋
Aba "Receber" não encontrada na planilha
Você precisa rodar a função inicializar no Apps Script para criar as novas abas.
1. Abra sua planilha → Extensões → Apps Script
2. Certifique que o código está atualizado (versão v4)
3. Selecione a função inicializar no menu
4. Clique em ▶️ Executar
5. Volte aqui e tente novamente
`; return; } const todaLista=res.lista||[]; // Contas já quitadas ficam arquivadas por mês de criação (não acumula // pra sempre na tela). Contas em aberto ou parciais aparecem sempre, // independente do mês, até serem resolvidas. const lista=todaLista.filter(r=>{ if(r.status!=='pago') return true; const ma=mesAnoDe(r.created_at); return ma && ma.mes===mes && ma.ano===ano; }); const filtrado=fil==='todos'?lista:lista.filter(r=>r.status===fil); const totAberto=filtrado.filter(r=>r.status!=='pago').reduce((s,r)=>s+Number(r.valor_total||0)-Number(r.valor_pago||0),0); setSS('ok','OK'); el('content').innerHTML=` ${periodNavHtml(mes,ano,'navReceberMes','hojeReceberMes')}
${totAberto>0?`
💰 Total a receber: ${fmt(totAberto)}
`:''} ${filtrado.length?filtrado.slice().reverse().map(r=>{ const restante=Number(r.valor_total||0)-Number(r.valor_pago||0); const pct=Number(r.valor_total||0)>0?Math.round(Number(r.valor_pago||0)/Number(r.valor_total||0)*100):0; return`
${r.cliente}
${r.veiculo||''} ${r.descricao?'· '+r.descricao:''}
${badgeRec(r.status)}
${r.status!=='pago'?``:''} ${isDono()?``:''}
Valor Total
${fmt(r.valor_total)}
Valor Recebido
${fmt(r.valor_pago)}
Restante
${fmt(restante)}
Progresso${pct}% recebido
${fmtD(r.created_at)}
`; }).join(''):`
Nenhuma conta a receber neste mês
`}`; }catch(e){setSS('e','Erro');el('content').innerHTML=`
⚠️ ${e.message}
`;} } function criarReceber(osId='',cliente='',veiculo='',valorTotal=''){ showM(`
${osId?`

📎 Vinculado à OS

`:''} `); } async function saveReceber(osId){ const c=gv('re-c'),v=gv('re-v'),d=gv('re-d'),vt=parseFloat(gv('re-vt'))||0,vp=parseFloat(gv('re-vp'))||0; if(!c||!vt){alert('Cliente e valor total obrigatórios.');return;} const status=vp>=vt?'pago':vp>0?'parcial':'aberto'; setSS('s','Salvando...'); await apiPost({acao:'inserir',tabela:'receber',dados:{os_id:osId||'',cliente:c,veiculo:v,descricao:d,valor_total:vt,valor_pago:vp,status}}); setSS('ok','Salvo');cM();loadReceber(); } function oPgtoModal(receberId,cliente,restante){ showM(`
Cliente: ${cliente} · Restante: ${fmt(restante)}

Digite apenas o valor que o cliente pagou agora — não precisa ser o total.

`); } async function savePgto(receberId){ const v=parseFloat(gv('pg-v'))||0,d=gv('pg-d'),o=gv('pg-o'); if(!v){alert('Digite o valor recebido.');return;} setSS('s','Salvando...'); await apiPost({acao:'adicionarPgtoRec',receber_id:receberId,valor:v,data:d,obs:o}); setSS('ok','Registrado');cM();loadReceber(); } async function delReceber(id){if(!confirm('Remover esta conta a receber?'))return;await apiPost({acao:'deletar',tabela:'receber',id});loadReceber();} // ===================================================== // CONTAS A PAGAR — flag pago/pendente destacado // ===================================================== function navContasMes(d){contasMes+=d;if(contasMes>12){contasMes=1;contasAno++;}if(contasMes<1){contasMes=12;contasAno--;}loadContas(contasFilAtual);} function hojeContasMes(){const h=new Date();contasMes=h.getMonth()+1;contasAno=h.getFullYear();loadContas(contasFilAtual);} let contasFilAtual='todos'; async function loadContas(fil='todos',mes=contasMes,ano=contasAno){ contasFilAtual=fil;contasMes=mes;contasAno=ano; setSS('s','...'); try{ const{lista}=await apiGet({acao:'listar',tabela:'contas'}); // Cada conta pertence ao mês/ano do seu vencimento. Sem vencimento // preenchido, ela cai no mês atual (hoje) — assim nada fica perdido // e cada mês continua isolado dos outros. const doMes=lista.filter(c=>{ const ma=mesAnoDe(c.vencimento); if(ma) return ma.mes===mes && ma.ano===ano; return ehMesAtualReal(mes,ano); }); const filtrado=fil==='todos'?doMes:doMes.filter(c=>c.status===fil); const totPend=doMes.filter(c=>c.status!=='pago').reduce((s,c)=>s+Number(c.valor||0),0); setSS('ok','OK'); el('content').innerHTML=` ${periodNavHtml(mes,ano,'navContasMes','hojeContasMes')}
${totPend>0?`
🧾 Total pendente em ${MN[mes-1]}/${ano}: ${fmt(totPend)}
`:''}
${filtrado.length?filtrado.slice().reverse().map(c=>``).join(''):``}
FornecedorDescriçãoValorVencimentoStatus
${c.fornecedor} ${c.descricao} ${fmt(c.valor)} ${fmtD(c.vencimento)} ${isDono()?`
`:''}
Nenhuma conta neste mês
`; }catch(e){setSS('e','Erro');el('content').innerHTML=`
⚠️ ${e.message}
`;} } async function toggleConta(id,statusAtual){ const novoStatus=statusAtual==='pago'?'pendente':'pago'; await apiPost({acao:'atualizar',tabela:'contas',id,dados:{status:novoStatus}}); loadContas(contasFilAtual); } function oContaModal(id='',forn='',desc='',val='',venc='',st='pendente'){ showM(`
`); } async function saveConta(id){ const f=gv('c-f'),d=gv('c-d'),val=parseFloat(gv('c-v'))||0,vc=gv('c-vc'),st=gv('c-st')||'pendente'; if(!f||!d){alert('Preencha todos os campos.');return;} const dados={fornecedor:f,descricao:d,valor:val,vencimento:vc||'',status:st}; if(id)await apiPost({acao:'atualizar',tabela:'contas',id,dados}); else await apiPost({acao:'inserir',tabela:'contas',dados}); cM();loadContas(contasFilAtual); } async function delConta(id){if(!confirm('Remover?'))return;await apiPost({acao:'deletar',tabela:'contas',id});loadContas(contasFilAtual);} // ===================================================== // CONTAS FIXAS — com pago/pendente por registro // ===================================================== function navFixasMes(d){fixasMes+=d;if(fixasMes>12){fixasMes=1;fixasAno++;}if(fixasMes<1){fixasMes=12;fixasAno--;}loadFixas();} function hojeFixasMes(){const h=new Date();fixasMes=h.getMonth()+1;fixasAno=h.getFullYear();loadFixas();} async function loadFixas(mes=fixasMes,ano=fixasAno){ fixasMes=mes;fixasAno=ano; const mesRef=`${ano}-${String(mes).padStart(2,'0')}`; setSS('s','...'); try{ const{lista}=await apiGet({acao:'listar',tabela:'fixas',mes_ref:mesRef}); const total=lista.reduce((s,f)=>s+Number(f.valor||0),0); const totalPend=lista.filter(f=>!f.status||f.status==='pendente').reduce((s,f)=>s+Number(f.valor||0),0); setSS('ok','OK'); el('content').innerHTML=` ${periodNavHtml(mes,ano,'navFixasMes','hojeFixasMes')}
Total do mês: ${fmt(total)}
${totalPend>0?`
⏳ Pendente: ${fmt(totalPend)}
`:lista.length?'
✅ Tudo pago!
':''}
${lista.length===0?``:''}
${lista.length?lista.map(f=>``).join(''):``}
DespesaValorFrequênciaDia PgtoStatus
${f.nome} ${fmt(f.valor)} ${f.frequencia||'Mensal'} Dia ${f.dia_pagamento||'--'} ${isDono()?`
`:''}
Nenhuma despesa fixa lançada nesse mês
`; }catch(e){setSS('e','Erro');el('content').innerHTML=`
⚠️ ${e.message}
`;} } async function copiarFixasMesAnterior(){ let m=fixasMes-1,a=fixasAno; if(m<1){m=12;a--;} const mesRefAnterior=`${a}-${String(m).padStart(2,'0')}`; const{lista}=await apiGet({acao:'listar',tabela:'fixas',mes_ref:mesRefAnterior}); if(!lista.length){alert('Não tem despesa fixa lançada no mês anterior pra copiar.');return;} if(!confirm(`Copiar ${lista.length} despesa(s) do mês anterior pra esse mês? (entram como Pendente)`))return; const mesRefAtual=`${fixasAno}-${String(fixasMes).padStart(2,'0')}`; for(const f of lista){ await apiPost({acao:'inserir',tabela:'fixas',dados:{nome:f.nome,valor:f.valor,frequencia:f.frequencia,dia_pagamento:f.dia_pagamento||'',status:'pendente',mes_ref:mesRefAtual}}); } loadFixas(); } async function toggleFixa(id,statusAtual){ const novoStatus=(!statusAtual||statusAtual==='pendente')?'pago':'pendente'; await apiPost({acao:'atualizar',tabela:'fixas',id,dados:{status:novoStatus}}); loadFixas(); } function oFixaModal(id='',nom='',val='',freq='Mensal',dia='',st='pendente'){ const mesRefPadrao=`${fixasAno}-${String(fixasMes).padStart(2,'0')}`; showM(`

Mesmo despesas fixas mudam de valor mês a mês (ex: conta de luz) — por isso cada mês tem seu próprio lançamento, editável.

`); } async function saveFixa(id){ const n=gv('f-n'),val=parseFloat(gv('f-v'))||0,fr=gv('f-fr'),d=gv('f-d'),st=gv('f-st')||'pendente',mr=gv('f-mr'); if(!n){alert('Nome obrigatório.');return;} if(!mr){alert('Escolha o mês de referência.');return;} const dados={nome:n,valor:val,frequencia:fr,dia_pagamento:d||'',status:st,mes_ref:mr}; if(id)await apiPost({acao:'atualizar',tabela:'fixas',id,dados}); else await apiPost({acao:'inserir',tabela:'fixas',dados}); cM();loadFixas(); } async function delFixa(id){if(!confirm('Remover?'))return;await apiPost({acao:'deletar',tabela:'fixas',id});loadFixas();} // ===================================================== // FOLHA // ===================================================== function navFolhaMes(d){folhaMes+=d;if(folhaMes>12){folhaMes=1;folhaAno++;}if(folhaMes<1){folhaMes=12;folhaAno--;}loadFolha();} function hojeFolhaMes(){const h=new Date();folhaMes=h.getMonth()+1;folhaAno=h.getFullYear();loadFolha();} async function loadFolha(mes=folhaMes,ano=folhaAno){ folhaMes=mes;folhaAno=ano; const mesRef=`${ano}-${String(mes).padStart(2,'0')}`; setSS('s','...'); try{ const{lista}=await apiGet({acao:'listar',tabela:'folha',mes_ref:mesRef}); const total=lista.reduce((s,f)=>s+Number(f.salario||0),0); const totalPend=lista.filter(f=>!f.status||f.status==='pendente').reduce((s,f)=>s+Number(f.salario||0),0); setSS('ok','OK'); el('content').innerHTML=` ${periodNavHtml(mes,ano,'navFolhaMes','hojeFolhaMes')}
Total do mês: ${fmt(total)}
${totalPend>0?`
⏳ Pendente: ${fmt(totalPend)}
`:lista.length?'
✅ Tudo pago!
':''}
${isDono()&&lista.length===0?``:''} ${isDono()?'':''}
${isDono()?'':''}${lista.length?lista.map(f=>` ${isDono()?``:''} `).join(''):``}
FuncionárioCargoSalárioDia PgtoStatus
${f.nome}${f.cargo} ${fmt(f.salario)}Dia ${f.dia_pagamento||'--'}
Nenhuma folha lançada nesse mês
`; }catch(e){setSS('e','Erro');el('content').innerHTML=`
⚠️ ${e.message}
`;} } async function copiarFolhaMesAnterior(){ let m=folhaMes-1,a=folhaAno; if(m<1){m=12;a--;} const mesRefAnterior=`${a}-${String(m).padStart(2,'0')}`; const{lista}=await apiGet({acao:'listar',tabela:'folha',mes_ref:mesRefAnterior}); if(!lista.length){alert('Não tem folha lançada no mês anterior pra copiar.');return;} if(!confirm(`Copiar ${lista.length} funcionário(s) do mês anterior pra esse mês? (entram como Pendente)`))return; const mesRefAtual=`${folhaAno}-${String(folhaMes).padStart(2,'0')}`; for(const f of lista){ await apiPost({acao:'inserir',tabela:'folha',dados:{nome:f.nome,cargo:f.cargo,salario:f.salario,dia_pagamento:f.dia_pagamento||'',status:'pendente',mes_ref:mesRefAtual}}); } loadFolha(); } async function toggleFolha(id,statusAtual){ const novoStatus=(!statusAtual||statusAtual==='pendente')?'pago':'pendente'; await apiPost({acao:'atualizar',tabela:'folha',id,dados:{status:novoStatus}}); loadFolha(); } function oFuncModal(id='',nom='',car='',sal='',dia=''){ const mesRefPadrao=`${folhaAno}-${String(folhaMes).padStart(2,'0')}`; showM(`

Salário pode variar mês a mês (hora extra, desconto, aumento) — por isso cada mês tem seu próprio lançamento.

`); } async function saveFunc(id){ const n=gv('fn-n'),c=gv('fn-c'),s=parseFloat(gv('fn-s'))||0,d=gv('fn-d'),mr=gv('fn-mr'); if(!n||!c){alert('Nome e cargo obrigatórios.');return;} if(!mr){alert('Escolha o mês de referência.');return;} if(id)await apiPost({acao:'atualizar',tabela:'folha',id,dados:{nome:n,cargo:c,salario:s,dia_pagamento:d,mes_ref:mr}}); else await apiPost({acao:'inserir', tabela:'folha',dados:{nome:n,cargo:c,salario:s,dia_pagamento:d,mes_ref:mr}}); cM();loadFolha(); } async function delFunc(id){if(!confirm('Remover?'))return;await apiPost({acao:'deletar',tabela:'folha',id});loadFolha();} // ===================================================== // USUÁRIOS // ===================================================== async function loadUsuarios(){ if(!isDono()){el('content').innerHTML=`
⚠️ Acesso restrito ao dono.
`;return;} setSS('s','...'); try{ const{lista}=await apiGet({acao:'listarUsuarios'}); setSS('ok','OK'); el('content').innerHTML=`
${lista.length} usuário(s)
💡 Também pode editar diretamente na aba Usuarios da planilha.
${lista.length?lista.map(u=>`
${(u.nome||'?')[0].toUpperCase()}
${u.nome} ${u.id===USER.id?'(você)':''}
@${u.usuario} · ${u.perfil==='agente'?'🛠️ Agente':u.perfil==='dono'?'👑 Dono':'👷 Funcionário'} · ${Number(u.ativo)===1?'Ativo':'Inativo'}
${u.id!==USER.id?``:''}
`).join(''):'
Nenhum usuário
'}
`; }catch(e){setSS('e','Erro');el('content').innerHTML=`
⚠️ ${e.message}
`;} } function oUserModal(id='',nom='',usu='',perf='funcionario',ativ='sim'){ showM(`
`); } async function saveUser(id){ const nm=gv('u-nm').trim(),us=gv('u-us').trim().toLowerCase(),pw=gv('u-pw').trim(),pf=gv('u-pf'),at=gv('u-at'); if(!nm||!us){alert('Nome e login obrigatórios.');return;} if(!id&&!pw){alert('Senha obrigatória para novo usuário.');return;} const dados={nome:nm,usuario:us,perfil:pf,ativo:at}; if(id)dados.id=id;if(pw)dados.senha=pw; const res=await apiPost({acao:'salvarUsuario',dados}); if(res.erro){alert(res.erro);return;} cM();loadUsuarios(); } async function delUser(id){if(!confirm('Remover este usuário?'))return;const r=await apiPost({acao:'deletarUsuario',id});if(r.erro){alert(r.erro);return;}loadUsuarios();} // ===================================================== // DADOS DA OFICINA — nome, telefone, logo // ===================================================== async function editName(){ if(!isDono())return; showM(`
${CFG.logo_url?``:''}
📤 Arraste uma imagem aqui ou clique pra escolher
JPG, PNG, GIF ou WEBP — máx 3MB
`); setTimeout(()=>{const i=el('new-name');if(i){i.focus();i.select();}setupLogoDrop();},60); } function setupLogoDrop(){ const drop=el('logo-drop'); if(!drop)return; drop.addEventListener('dragover',e=>{e.preventDefault();drop.style.borderColor='var(--acc)';}); drop.addEventListener('dragleave',()=>{drop.style.borderColor='var(--brd2)';}); drop.addEventListener('drop',e=>{ e.preventDefault();drop.style.borderColor='var(--brd2)'; if(e.dataTransfer.files.length)uploadLogoFile(e.dataTransfer.files[0]); }); } async function uploadLogoFile(file){ if(!file)return; const msg=el('logo-msg'); msg.style.display='block';msg.style.color='var(--tx2)';msg.textContent='Enviando...'; const fd=new FormData(); fd.append('acao','uploadLogo');fd.append('token',TOKEN);fd.append('logo',file); try{ const r=await fetch(API_URL,{method:'POST',body:fd}); const d=await r.json(); if(d.erro){msg.style.color='var(--red)';msg.textContent=d.erro;return;} await apiPost({acao:'salvarConfig',dados:{logo_url:d.url}}); CFG.logo_url=d.url; el('logo-preview').innerHTML=``; atualizarCabecalhoOficina(); msg.style.color='var(--grn)';msg.textContent='✅ Logo atualizada!'; }catch(e){msg.style.color='var(--red)';msg.textContent='Erro: '+e.message;} } async function saveDadosOficina(){ const n=gv('new-name'),tel=gv('new-tel'),end=gv('new-end'),cnpj=gv('new-cnpj'); if(!n){alert('Digite um nome.');return;} await apiPost({acao:'salvarConfig',dados:{nome_oficina:n,telefone:tel,endereco:end,cnpj:cnpj}}); CFG.nome_oficina=n;CFG.telefone=tel;CFG.endereco=end;CFG.cnpj=cnpj;atualizarCabecalhoOficina();cM(); } // ===================================================== // LINK DE AVALIAÇÃO (Google) — sugerido ao concluir uma OS // ===================================================== function linkAvaliacaoModal(){ if(!isDono())return; showM(`

Esse link é sugerido automaticamente pro cliente quando uma OS chega na etapa "Retirada". Pra pegar o seu: busca sua oficina no Google Maps → "Peça uma avaliação" → copia o link.

`); } async function salvarLinkAvaliacao(){ const url=gv('la-url').trim(); await apiPost({acao:'salvarConfig',dados:{link_avaliacao_google:url}}); CFG.link_avaliacao_google=url; const msg=el('la-msg');msg.textContent='✅ Salvo!';msg.className='smsg smsg-o';msg.style.display='block'; setTimeout(()=>cM(),700); }