Carregando a gestão Arts Ney…
`);w.document.close()} function empHd(t,sub){const e=cfg('empresa');return `

${esc(e.nome)}

${esc([e.doc,e.fone,e.email].filter(Boolean).join(' · '))}
${esc(e.endereco||'')}

${esc(t)}

${sub||''}
`} function docComercial(tp,id,o){const T=tp==='orc'?'Orcamento':'Pedido',d=get(T,id),its=docItems(tp,id),c=docCalc(d,its),cl=get('Cliente',d.cliente_id)||{}; let h=empHd(tp==='orc'?'ORÇAMENTO':'PEDIDO',`${fmtNum(T,d.numero)}${num(d.versao)>1?' v'+d.versao:''}
Data: ${fdate(d.data)}${tp==='orc'?'
Válido até '+fdate(addDays(d.data,num(d.validade_dias)||7)):'
Entrega prevista: '+fdate(d.entrega_prevista)}`); h+=`
Cliente: ${esc(cl.fantasia||cl.nome||'')} ${cl.razao_social&&cl.razao_social!==cl.fantasia?'('+esc(cl.razao_social)+')':''}
${esc([cl.doc,cl.contato,cl.whatsapp,cl.email].filter(Boolean).join(' · '))}
`; h+=`${o.fotos?'':''}${its.map((i,k)=>{const l=lastLayouts(i).find(x=>x.tipo!=='link');return `${o.fotos?``:''}`}).join('')}${c.desc?``:''}${c.ajuste?``:''}${c.frete?``:''}
#LayoutProduto / especificaçãoQtdUnitárioTotal
${k+1}${l?``:''}${esc(i.descricao)}${i.especificacao?'
'+esc(i.especificacao):''}${i.grade?'
Grade: '+esc(gradeTxt(i.grade))+'':''}
${qf(i.quantidade)} ${esc(i.unidade||'')}${brl(i.preco_unit)}${brl(itemVenda(i))}
Subtotal${brl(c.bruto)}
Desconto− ${brl(c.desc)}
Ajuste da forma de pagamento${brl(c.ajuste)}
Frete${brl(c.frete)}
TOTAL${brl(c.total)}
`; if(tp==='orc'){const L=condLinhas(d.condicoes,c.liquido);if(L.length)h+=`

Condições de pagamento

${L.map(l=>``).join('')}
${esc(l.t)}${esc(l.det)}${brl(l.v+c.frete)}
`;if(num(d.condicoes&&d.condicoes.sinal_pct))h+=`

Sinal para início: ${num(d.condicoes.sinal_pct)}% do valor.

`;h+=`

Prazo: ${num(d.prazo_producao)||'—'} dias úteis de produção após aprovação${d.prazo_entrega?' · '+esc(d.prazo_entrega):''}

`} else{const f=finPedido(d);h+=`

Pagamento

${esc(d.forma_acordada||'')}
Pago: ${brl(f.rec)} · Saldo: ${brl(f.sal)}

`} if(o.fotos){const big=its.filter(i=>lastLayouts(i).some(l=>l.tipo!=='link'));if(big.length)h+=`

Layouts para conferência

${big.map(i=>lastLayouts(i).filter(l=>l.tipo!=='link').map(l=>`
${esc(i.descricao)} — versão ${l.v}
`).join('')).join('')}`} if(d.obs_publica)h+=`

Observações

${esc(d.obs_publica).replace(/\n/g,'
')}

`; h+=`
${esc(cfg('empresa').nome)}
De acordo — ${esc(cl.fantasia||cl.nome||'Cliente')}

${esc(cfg('empresa').rodape||'')}

`;return h} function printOrcamento(id,o={}){printWin('Orçamento',docComercial('orc',id,o))} function printPedido(id,o={}){printWin('Pedido',docComercial('ped',id,o))} function printOP(id,o={}){const p=get('Pedido',id),its=docItems('ped',id),cl=get('Cliente',p.cliente_id)||{}; printWin('Ordem de produção',empHd('ORDEM DE PRODUÇÃO',`${fmtNum('Pedido',p.numero)}
Entrega: ${fdate(p.entrega_prevista)}
Prioridade: ${(PRI[p.prioridade]||['—'])[0]}`)+`
Cliente: ${esc(cl.fantasia||cl.nome||'')} ${p.obs_interna?'
Obs. internas: '+esc(p.obs_interna):''}
`+ its.map((i,k)=>{const l=lastLayouts(i).find(x=>x.tipo!=='link'),pend=[...(!layoutOk(i)?['Layout não aprovado']:[]),...materialPend(i)];return `

Item ${k+1}: ${esc(i.descricao)} — ${qf(i.quantidade)} ${esc(i.unidade||'')}

${esc(i.especificacao||'')}

${i.grade?gradeTable(gradeMerge([i.grade])).replace(/class="tbl"/,''):''}

Etapa atual: ${etNome(i.etapa)} · Layout: ${lastV(i)?'v'+lastV(i)+(layoutOk(i)?' aprovado por '+esc((i.layout_aprov||{}).por||''):' — AGUARDANDO APROVAÇÃO'):'não exigido/enviado'}

${pend.length?`

Pendências: ${pend.map(esc).join('; ')}

`:''}

Materiais: ${(i.custos||[]).filter(c=>c.tipo!=='Criação de arte').map(c=>`${esc(c.desc)} (${qf(lineQty(c,i))} ${esc(c.un||'')})`).join(' · ')}

${o.fotos&&l?``:''}

Conferido por: _______________________ Data: ___/___/____

`}).join(''))} function printCompra(id){const c=get('Compra',id),f=get('Fornecedor',c.fornecedor_id)||{};const gs=c.linhas.filter(l=>l.grade).map(l=>l.grade); printWin('Pedido de compra',empHd('PEDIDO DE COMPRA',`${fmtNum('Compra',c.numero)}
Data: ${fdate(c.data)}${c.previsao?'
Previsão: '+fdate(c.previsao):''}`)+`
Fornecedor: ${esc(f.nome||'')} ${f.razao_social?'('+esc(f.razao_social)+')':''}
${esc([f.doc,f.contatos,f.whatsapp,f.email].filter(Boolean).join(' · '))}
${c.linhas.map(l=>``).join('')}${num(c.frete)?``:''}${num(c.desconto)?``:''}
ItemEspecificaçãoQtdUnitárioTotal
${esc(l.desc)}${l.variacao?' · '+esc(l.variacao):''}${l.nec_id?esc(pedNum((get('Necessidade',l.nec_id)||{}).pedido_id))+' · ':''}${esc(((get('Necessidade',l.nec_id)||{}).especificacao)||'')}${l.grade?'
'+esc(gradeTxt(l.grade))+'':''}
${qf(l.qtd)} ${esc(l.un||'')}${brl(l.unit)}${brl(l.bruto)}
Subtotal${brl(c.subtotal)}
Frete${brl(c.frete)}
Desconto− ${brl(c.desconto)}
TOTAL${brl(c.total)}
${gs.length?`

Grade consolidada (todos os pedidos)

${gradeTable(gradeMerge(gs)).replace(/class="tbl"/,'')}`:''}

Recebimento: ${esc(RECB[c.recebimento]||'')}${c.balcao?' — '+esc(c.balcao):''}${c.obs_recebimento?'
'+esc(c.obs_recebimento):''}

`)} function printClienteFin(cid){const c=get('Cliente',cid),ts=rows('Titulo',true).filter(t=>t.cliente_id==cid&&t.tipo==='receber'&&t.status!=='cancelado'),cr=rows('Credito',true).filter(x=>x.cliente_id==cid); printWin('Resumo financeiro',empHd('RESUMO FINANCEIRO',`${fdate(today())}`)+`
${esc(c.fantasia||c.nome)} ${esc(c.razao_social||'')}
${esc([c.doc,c.whatsapp,c.email].filter(Boolean).join(' · '))}
${ts.map(t=>``).join('')}
VencimentoDescriçãoValorPagoSaldo
${fdate(t.vencimento)}${esc(t.descricao)}${brl(t.valor)}${brl(pagoTit(t))}${brl(saldoTit(t))}
Totais${brl(sum(ts,t=>t.valor))}${brl(sum(ts,pagoTit))}${brl(sum(ts,saldoTit))}
${cr.length?`

Créditos

Crédito disponível: ${brl(creditoCliente(cid))}

`:''}${cfg('empresa').pix?`

PIX para pagamento: ${esc(cfg('empresa').pix)}

`:''}`)} async function relacaoModal(){return modal({title:'Relação de pedidos',body:fhtml([{k:'t',l:'Quais pedidos',t:'select',opts:[['abertos','Com saldo a receber'],['pagos','Pagos'],['ativos','Em produção (ativos)'],['todos','Todos']]},{k:'a',l:'De',t:'date'},{k:'b',l:'Até',t:'date'}]),actions:[{label:'Gerar',fn:w=>{const t=$('#f_t',w).value,a=$('#f_a',w).value,b=$('#f_b',w).value;const ps=rows('Pedido').filter(p=>p.status!=='cancelado'&&(!a||p.data>=a)&&(!b||p.data<=b)).filter(p=>{const f=finPedido(p);return t==='abertos'?f.sal>0.004:t==='pagos'?f.st==='pago':t==='ativos'?p.status==='ativo':true}).sort((x,y)=>x.numero-y.numero); printWin('Relação de pedidos',empHd('RELAÇÃO DE PEDIDOS',({abertos:'Com saldo a receber',pagos:'Pagos',ativos:'Ativos',todos:'Todos'})[t]+(a||b?`
${fdate(a)} a ${fdate(b)}`:''))+`${ps.map(p=>{const f=finPedido(p);return ``}).join('')}
PedidoClienteDataEntregaProduçãoTotalRecebidoSaldo
${fmtNum('Pedido',p.numero)}${esc(cliNome(p.cliente_id))}${fdate(p.data)}${fdate(p.entrega_prevista)}${etNome(pedEtapa(p))}${brl(p.total)}${brl(f.rec)}${brl(f.sal)}
${ps.length} pedidos${brl(sum(ps,p=>p.total))}${brl(sum(ps,p=>finPedido(p).rec))}${brl(sum(ps,p=>finPedido(p).sal))}
`)}}]})} /* ===================== RELATÓRIOS / DRE ===================== */ VIEWS.relatorios={title:'Relatórios e DRE',render(el){const hj=today(); el.innerHTML=`
Período de competência
`; $('#rc').onchange=e=>{if(e.target.value)printClienteFin(e.target.value);e.target.value=''}; const draw=()=>{const a=$('#ra').value,b=$('#rb').value,inP=d=>d&&d>=a&&d<=b; const peds=rows('Pedido').filter(p=>p.status!=='cancelado'&&inP(p.data));let rec=0,fr=0,imp=0,cmv=0,ok=true;const cat={}; peds.forEach(p=>{const its=docItems('ped',p.id),c=docCalc(p,its);rec+=c.receita;fr+=c.frete;imp+=c.imp;cmv+=c.efet;if(!c.ok)ok=false;its.forEach(i=>{const k=i.categoria||'Sem categoria';cat[k]=cat[k]||{v:0,c:0};cat[k].v+=itemVenda(i);cat[k].c+=itemCusto(i).efet})}); const desp={};rows('Titulo').filter(t=>t.tipo==='pagar'&&t.status!=='cancelado'&&inP(t.competencia)&&!['Compra p/ pedido','Compra p/ estoque'].includes(t.categoria)).forEach(t=>{const k=t.categoria||'Outros';desp[k]=num(desp[k])+num(t.valor)}); const outras=sum(rows('Titulo').filter(t=>t.tipo==='receber'&&!t.pedido_id&&t.status!=='cancelado'&&inP(t.competencia)),t=>t.valor); const tdesp=sum(Object.values(desp),x=>x),rl=r2(rec-imp),lb=r2(rl-cmv),res=r2(lb-tdesp+outras); const recebido=sum(rows('Baixa',true).filter(x=>!bool(x.estornado)&&inP(x.data)&&(get('Titulo',x.titulo_id)||{}).tipo==='receber'&&x.forma!=='credito_cliente'),x=>x.valor)+sum(rows('Credito',true).filter(x=>x.conta_id&&num(x.valor)>0&&inP(x.data)),x=>x.valor); const pago=sum(rows('Baixa',true).filter(x=>!bool(x.estornado)&&inP(x.data)&&(get('Titulo',x.titulo_id)||{}).tipo==='pagar'&&x.conta_id),x=>x.valor)+sum(rows('MovConta',true).filter(m=>m.tipo==='pag_fatura'&&!bool(m.estornado)&&inP(m.data)),m=>m.valor); const estq=sum(rows('Titulo').filter(t=>t.categoria==='Compra p/ estoque'&&t.status!=='cancelado'&&inP(t.competencia)),t=>t.valor); const L=(t,v,k='')=>`
${t}${brl(v)}
`; $('#rw').innerHTML=`

DRE gerencial (competência)

${L('Receita bruta de vendas (pedidos)',rec)}${L('(−) Impostos estimados',-imp,'neg')}${L('(=) Receita líquida',rl,'t')}${L('(−) Custo dos produtos/serviços vendidos'+(ok?'':' *'),-cmv,'neg')}${L('(=) Lucro bruto',lb,'t')}
Margem bruta${pct(rl?lb/rec*100:0)}
${Object.entries(desp).sort((x,y)=>y[1]-x[1]).map(([k,v])=>L('(−) '+esc(k),-v,'neg')).join('')}${outras?L('(+) Outras receitas',outras):''}${L('(=) Resultado operacional',res,'t')}${L('Frete cobrado (repasse, fora do resultado)',fr)}${ok?'':'

* Parte do custo ainda é previsto (compras não realizadas ou estoque não baixado).

'}

Faturamento × recebimento × lucro × banco

${L('Faturamento (pedidos no período)',rec+fr)}${L('Recebido em caixa no período',recebido)}${L('Pago em caixa no período (inclui faturas de cartão)',-pago,'neg')}${L('Lucro (resultado operacional)',res)}${L('Saldo bancário/caixa hoje',sum(contas(),c=>saldoConta(c.id)),'t')}${L('Contas a receber em aberto (hoje)',sum(rows('Titulo').filter(t=>t.tipo==='receber'),saldoTit))}${L('Contas a pagar em aberto (hoje)',sum(rows('Titulo').filter(t=>t.tipo==='pagar'),saldoTit))}${L('Compras para estoque no período (vira custo só quando consumido)',estq)}

Vendas por categoria

${tbl(['Categoria','Vendas','Custo','Margem'],Object.entries(cat).map(([k,x])=>[esc(k),brl(x.v),brl(x.c),bd(pct(x.v?(x.v-x.c)/x.v*100:0),saude(x.v?(x.v-x.c)/x.v*100:0)[1])]))}

Critérios usados (documentação)

`}; $('#ra').onchange=draw;$('#rb').onchange=draw;$('#rm').onclick=()=>{$('#ra').value=hj.slice(0,8)+'01';$('#rb').value=hj;draw()};$('#ry').onclick=()=>{$('#ra').value=hj.slice(0,4)+'-01-01';$('#rb').value=hj;draw()};draw()}}; /* ===================== CONFIGURAÇÕES ===================== */ VIEWS.config={title:'Configurações',render(el,pp){const tab=pp[0]||'empresa'; el.innerHTML=`
${[['empresa','Empresa'],['impostos','Impostos'],['usuarios','Usuários e senhas'],['listas','Grade e listas'],['ambiente','Ambiente e links']].map(([k,l])=>``).join('')}
`;const w=$('#cw'); if(tab==='empresa'){const fs=[{k:'nome',l:'Nome da empresa',req:true},{k:'doc',l:'CNPJ/CPF'},{k:'fone',l:'Telefone/WhatsApp'},{k:'email',l:'E-mail'},{k:'endereco',l:'Endereço',span:1},{k:'pix',l:'Chave PIX (aparece nos resumos ao cliente)'},{k:'rodape',l:'Rodapé dos documentos'}];w.innerHTML=`

Dados da empresa (aparecem nos documentos e no link do cliente)

${fhtml(fs,cfg('empresa'))}`;$('#sv').onclick=async()=>{await saveCfg('empresa',fread(fs,w));toast('Salvo')}} if(tab==='impostos'){const c=cfg('impostos');const fs=[{k:'venda',l:'Venda / revenda',t:'num',suf:'%'},{k:'fabricacao',l:'Fabricação / manipulação',t:'num',suf:'%'},{k:'servico',l:'Serviço',t:'num',suf:'%'},{k:'padrao',l:'Padrão em orçamentos e pedidos',t:'select',opts:Object.entries(IMP)}];w.innerHTML=`

Margem de imposto por tipo de operação

Usada no cálculo de lucro e margem (Saúde do orçamento, pedidos, DRE). Cada item pode ter seu tipo; o padrão é Fabricação/manipulação.

${fhtml(fs,c)}`;$('#sv').onclick=async()=>{await saveCfg('impostos',fread(fs,w));toast('Impostos salvos')}} if(tab==='usuarios'){let us=clone(cfg('usuarios'));const draw=()=>{w.innerHTML=`

Usuários do sistema

A senha é usada para entrar na gestão neste aparelho e para autorizar mudanças de etapa na produção, ajustes de estoque e avanços forçados. A gestão em si continua protegida pelo login da conta proprietária do wish4 (servidor).

${tbl(['Nome','Função / acesso','Ativo','Senha',''],us.map((u,i)=>[``,``,``,``,``]))}

Acessos: Comercial (CRM, orçamentos, pedidos, clientes), Produção (produção, compras, estoque, entregas), Financeiro (financeiro, relatórios, compras). Administrador vê tudo, inclusive configurações, histórico e backup.

`};draw(); w.oninput=e=>{const t=e.target;if(t.dataset.u){const[i,k]=t.dataset.u.split('|');us[+i][k]=t.type==='checkbox'?t.checked:t.value}};w.onchange=e=>{const t=e.target;if(t.dataset.u&&t.type==='checkbox'){const[i,k]=t.dataset.u.split('|');us[+i][k]=t.checked}}; w.onclick=async e=>{const t=e.target;if(t.id==='ua'){us.push({id:uid(),nome:'',papel:us.length?'producao':'admin',ativo:true});draw()}if(t.dataset.ur!=null){if(await confirmBox('Remover este usuário?')){us.splice(+t.dataset.ur,1);draw()}} if(t.id==='us'){for(const[i,u]of us.entries()){const pw=$(`[data-pw="${i}"]`,w).value;if(!u.nome)return toast('Informe o nome de todos os usuários','er');if(pw){if(pw.length<4)return toast('Senha com no mínimo 4 caracteres','er');u.hash=await pwHash(u,pw)}if(!u.hash)return toast('Defina a senha de '+u.nome,'er')} if(us.length&&!us.some(u=>u.papel==='admin'&&u.ativo!==false))return toast('Mantenha ao menos um administrador ativo','er'); if(us.length&&!(DB.Config||[]).some(x=>x.chave==='usuarios'&&(x.valor||[]).length)&&!await confirmBox('A partir de agora a gestão pedirá usuário e senha ao abrir. Continuar?'))return; await saveCfg('usuarios',us);if(!us.find(u=>S.user&&u.id===S.user.id)){const a=us.find(u=>u.papel==='admin');if(a){S.user=a;SS.setItem('an_uid',a.id)}}shell();toast('Usuários salvos')}}} if(tab==='listas'){const g=cfg('grade');w.innerHTML=`

Grade de tamanhos

${fhtml([{k:'gt',l:'Tamanhos (na ordem, separados por vírgula)',def:g.tamanhos.join(', '),span:1},{k:'cd',l:'Categorias de despesa',def:cfg('catDespesa').join(', '),t:'area',span:1},{k:'ct',l:'Tipos de custo da composição',def:cfg('composicaoTipos').join(', '),t:'area',span:1}])}`;const sp=s=>s.split(',').map(x=>x.trim()).filter(Boolean);$('#sv').onclick=async()=>{await saveCfg('grade',{tamanhos:sp($('#f_gt').value)});await saveCfg('catDespesa',sp($('#f_cd').value));await saveCfg('composicaoTipos',sp($('#f_ct').value));toast('Salvo')}} if(tab==='ambiente'){const nd=TABLES.reduce((a,t)=>a+(DB[t]||[]).filter(isDemo).length,0);w.innerHTML=`

Dados reais × demonstração

Você está em: ${S.demo?'DEMONSTRAÇÃO':'DADOS REAIS'}. Registros de demonstração ficam marcados e nunca aparecem no ambiente real (nem nos relatórios).

Links de aprovação

${fhtml([{k:'lv',l:'Validade padrão',t:'num',def:cfg('links').validade,suf:'dias'}])}`; $('#tg').onclick=()=>setDemo(!S.demo);$('#ls').onclick=async()=>{await saveCfg('links',{validade:num($('#f_lv').value)||15});toast('Salvo')}; $('#wp').onclick=async()=>{if(!nd)return toast('Não há dados de demonstração');if(!await confirmBox(`Apagar definitivamente ${nd} registro(s) de DEMONSTRAÇÃO? Dados reais não são afetados.`,{cls:'dg',label:'Apagar demonstração'}))return;let n=0;for(const t of TABLES)for(const r of (DB[t]||[]).filter(isDemo)){try{await req('DELETE',`data/${t}/${r.id}`);n++}catch(e){}$('#wp').textContent='Apagando… '+n}await loadAll();toast(n+' registros de demonstração apagados');render()}}}}; /* ===================== HISTÓRICO ===================== */ VIEWS.historico={title:'Histórico de operações',async render(el){el.innerHTML='

Carregando histórico…

'; let ls=[];try{const d=await req('GET','data/Config?tipo=log&sort=created_at&order=desc&limit=500');ls=(d.records||[]).filter(l=>bool(l.demo)===S.demo)}catch(e){el.innerHTML='
'+esc(e.message)+'
';return} el.innerHTML='

Últimas 500 operações: quem fez, quando, o que mudou (valor anterior → novo) e o motivo quando exigido.

'; const ents=[...new Set(ls.map(l=>l.entidade))],uss=[...new Set(ls.map(l=>l.usuario))]; mkList($('#hl'),{rows:()=>ls,search:l=>[l.entidade,l.acao,l.usuario,l.detalhes].join(' '),range:l=>String(l.data).slice(0,10),filters:[{k:'e',l:'Área',opts:ents.map(e=>[e,e]),test:(l,v)=>l.entidade===v},{k:'u',l:'Usuário',opts:uss.map(u=>[u,u]),test:(l,v)=>l.usuario===v}], cols:[{h:'Quando',v:l=>`${fdt(l.data)}`},{h:'Usuário',v:l=>esc(l.usuario)},{h:'Área',v:l=>bd(l.entidade+(l.ref_id?' #'+l.ref_id:''),'pr')},{h:'Ação',v:l=>`${esc(l.acao)}`},{h:'O que mudou',v:l=>{let d={};try{d=JSON.parse(l.detalhes||'{}')}catch(e){d={m:l.detalhes}}return `
${d.m?esc(d.m):''}${d.motivo?`
Motivo: ${esc(d.motivo)}
`:''}${d.d?Object.entries(d.d).map(([k,v])=>`
${esc(k)}: ${esc(v[0])} → ${esc(v[1])}
`).join(''):''}
`}}]})}}; /* ===================== BACKUP ===================== */ const ORDER=['Conta','Cliente','Fornecedor','Produto','Oportunidade','Orcamento','Pedido','Item','Necessidade','Compra','Titulo','Baixa','Credito','CartaoLanc','MovConta','Movimento','Entrega','Quadro']; const FK={cliente_id:'Cliente',fornecedor_id:'Fornecedor',produto_id:'Produto',orcamento_id:'Orcamento',pedido_id:'Pedido',oportunidade_id:'Oportunidade',item_id:'Item',compra_id:'Compra',titulo_id:'Titulo',baixa_id:'Baixa',conta_id:'Conta',conta_destino_id:'Conta',cartao_id:'Conta',pagamento_id:'MovConta',origem_id:'Necessidade',quadro_id:'Quadro'}; async function exportBackup(logs){const o={app:'artsney-gestao',versao:2,gerado:nowISO(),projeto:PID,tabelas:{}};for(const t of ORDER)o.tabelas[t]=await loadTable(t);o.config=(await loadTable('Config','&tipo=cfg'));if(logs)o.logs=await loadTable('Config','&tipo=log'); const blob=new Blob([JSON.stringify(o)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`backup-artsney-${today()}.json`;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),5000);logOp('Backup',null,'exportou backup',{m:Object.entries(o.tabelas).map(([t,r])=>t+':'+r.length).join(' ')});return o} async function restoreBackup(o,{cfgToo,logsToo},prog){if(!o||o.app!=='artsney-gestao'||!o.tabelas)throw new Error('Arquivo não é um backup válido da gestão Arts Ney.'); const M={};const pend=[];let n=0;const total=ORDER.reduce((a,t)=>a+(o.tabelas[t]||[]).length,0); const mapId=(t,id)=>id==null||id===''?null:(M[t]&&M[t][id])||null; for(const t of ORDER){M[t]={};for(const r of o.tabelas[t]||[]){const x=clone(r);const old=x.id;let need=false; for(const[f,tt]of Object.entries(FK))if(x[f]!=null&&x[f]!==''){const m=mapId(tt,x[f]);if(m)x[f]=m;else{x['__'+f]=x[f];x[f]=null;need=true}} if(t==='Item'){const m=mapId(x.doc_tipo==='orc'?'Orcamento':'Pedido',x.doc_id);x.doc_id=m} if(t==='Movimento'&&x.ref_id){const tt=x.ref_tipo==='compra'?'Compra':x.ref_tipo==='pedido'?'Pedido':null;x.__ref=x.ref_id;x.ref_id=tt?mapId(tt,x.ref_id):null;if(tt&&!x.ref_id)need=true} if(['Item','Produto','Compra','Entrega','Orcamento','Pedido'].includes(t))need=true; const keep={};Object.keys(x).filter(k=>k.startsWith('__')).forEach(k=>{keep[k]=x[k];delete x[k]}); const d=await req('POST','data/'+t,enc(t,x));const nr=dec(t,{...(d.record||d)});M[t][old]=nr.id;if(need)pend.push([t,nr.id,{...x,...keep}]);n++;if(prog&&n%10===0)prog(`Inserindo ${n}/${total}…`)}} let k=0;for(const[t,id,x]of pend){const ch={};for(const f of Object.keys(FK))if(x['__'+f]!=null)ch[f]=mapId(FK[f],x['__'+f]); if(t==='Movimento'&&x.__ref)ch.ref_id=mapId(x.ref_tipo==='compra'?'Compra':'Pedido',x.__ref); if(t==='Item'&&x.custos)ch.custos=x.custos.map(c=>({...c,fornecedor_id:mapId('Fornecedor',c.fornecedor_id),produto_id:mapId('Produto',c.produto_id),consumo:c.consumo?{...c.consumo,movs:[]}:null})); if(t==='Produto'&&x.composicao)ch.composicao=x.composicao.map(c=>({...c,fornecedor_id:mapId('Fornecedor',c.fornecedor_id),produto_id:mapId('Produto',c.produto_id)})); if(t==='Compra'&&x.linhas)ch.linhas=x.linhas.map(l=>({...l,nec_id:l.nec_id?mapId('Necessidade',l.nec_id):undefined,produto_id:l.produto_id?mapId('Produto',l.produto_id):undefined})); if(t==='Entrega'&&x.itens)ch.itens=x.itens.map(i=>({...i,item_id:mapId('Item',i.item_id)})); if((t==='Orcamento'||t==='Pedido')&&x.aprovacao&&x.aprovacao.status==='aprovado'){/* hash por conteúdo: aprovação permanece válida */} if(Object.keys(ch).length)await req('PATCH',`data/${t}/${id}`,enc(t,ch));k++;if(prog&&k%10===0)prog(`Religando vínculos ${k}/${pend.length}…`)} if(cfgToo)for(const c of o.config||[]){const ex=(DB.Config||[]).find(x=>x.chave===c.chave);if(ex)await req('PATCH','data/Config/'+ex.id,{valor:typeof c.valor==='string'?c.valor:JSON.stringify(c.valor)});else await req('POST','data/Config',{tipo:'cfg',chave:c.chave,valor:typeof c.valor==='string'?c.valor:JSON.stringify(c.valor),demo:false})} if(logsToo)for(const l of o.logs||[]){const tt=FK[Object.keys(FK).find(k=>FK[k]===l.entidade)]?l.entidade:null;await req('POST','data/Config',{...enc('Config',l),ref_id:tt?mapId(tt,l.ref_id):l.ref_id})} await logOp('Backup',null,'restaurou backup',{m:`${n} registros de ${fdate(String(o.gerado).slice(0,10))}`});return n} VIEWS.backup={title:'Backup e restauração',render(el){const cnt=TABLES.map(t=>[t,(DB[t]||[]).length]); el.innerHTML=`

Exportar backup

Gera um arquivo .json com todos os registros (dados reais e de demonstração, cada um marcado), configurações e, opcionalmente, o histórico.

Registros atuais

${cnt.map(([t,n])=>t+': '+n).join(' · ')}

Restaurar backup

A restauração adiciona os registros do arquivo e religa todos os vínculos (pedidos, itens, compras, pagamentos, estoque). Ela nunca apaga a base atual — por isso evite restaurar duas vezes o mesmo arquivo na mesma base.

Recomendado para migrar para outro projeto vazio ou recuperar uma base perdida. Faça um backup antes.

`; let data=null;$('#be').onclick=async()=>{$('#be').disabled=true;try{await exportBackup($('#bl').checked);toast('Backup gerado')}catch(e){toast(e.message,'er')}$('#be').disabled=false}; $('#bf').onchange=async e=>{const f=e.target.files[0];if(!f)return;try{data=JSON.parse(await f.text());if(data.app!=='artsney-gestao')throw new Error('Arquivo inválido');$('#bp').innerHTML=`Backup de ${fdt(data.gerado)}: ${Object.entries(data.tabelas).map(([t,r])=>t+' '+r.length).join(' · ')}`;$('#br').disabled=false}catch(x){$('#bp').textContent='Arquivo inválido: '+x.message;data=null;$('#br').disabled=true}}; $('#br').onclick=async()=>{const tot=Object.values(data.tabelas).reduce((a,r)=>a+r.length,0);const cur=TABLES.reduce((a,t)=>a+(DB[t]||[]).length,0);if(!await confirmBox(`Restaurar ${tot} registros?${cur?`
Atenção: a base atual já tem ${cur} registros — eles serão mantidos e os do backup serão adicionados.`:''}`,{label:'Restaurar'}))return;$('#br').disabled=true;try{const n=await restoreBackup(data,{cfgToo:$('#bc').checked,logsToo:$('#blg').checked},m=>$('#bp').textContent=m);await loadAll();toast(n+' registros restaurados');render()}catch(e){toast(e.message,'er');$('#bp').textContent='Falhou: '+e.message+' — registros já inseridos foram mantidos.'}}}};