CI Digital
Controle Interno Municipal

📊 Dashboard — Controle Interno

🏛️ — ✏️ Editor 👤 —
Total Registros 📋
0
Este ano
Valor Total Pago 💰
R$ 0
No período
Pareceres Favoráveis
0%
Alertas de Risco 🚨
0
Fracionamento / limites
Pareceres Contrários
0
Requerem atenção
Fornecedores Únicos 🏢
0
No período

📈 Evolução 12 Meses — Ano Atual vs Anterior

💰 Valor por Unidade Gestora

📈 Evolução Mensal de Pagamentos

📊 Distribuição por Parecer

🔧 Top 10 Fornecedores (Valor)

📋 Últimos 10 Registros

DataUnidade Gestora FornecedorTipo ServiçoValor ParecerRisco
➕ Novo Registro de Pagamento
💡 Cadastre as unidades em Administração → Parametrização → Unidades Gestoras.
💡 Escolha um da lista ou digite um novo se não encontrar (até 500 caracteres).
💡 Para compra verbal, selecione "Não se aplica".

👥 Fornecedores Cadastrados

TipoDocumentoRazão Social / Nome Nome FantasiaMunicípio/UFSituaçãoAções

📋 Registros de Pagamento

DataUnidade Gestora FornecedorTipo Serviço ModalidadeVerbal?Licitação?Contrato?Valor ParecerRiscoAções
⚠️ Análise de Riscos e Conformidade
Fracionamentos✂️
0
Possíveis casos detectados
Acima do Limite📊
0
Valores acima de R$50k sem licitação
Sem Modalidade📝
0
Registros sem modalidade informada
Verbais Acima do Limite🚨
0
Acima de R$ 13.098,41 — irregular
✂️

Possível Fracionamento de Compras

Nenhum fracionamento detectado

⚠️

Valores Acima do Limite de Dispensa

Nenhuma irregularidade detectada

🗣️

Fracionamento via Compras Verbais Art. 8º §1º Lei 14.133

Nenhum fracionamento verbal detectado

🚨

Compras Verbais Acima do Limite R$ 13.098,41

Todas as compras verbais dentro do limite

📅

Falta de Planejamento — 3+ Compras Verbais/Mês por Secretaria

Sem falhas de planejamento identificadas

🔄

Fornecedores com Alto Volume

Sem concentração excessiva

🗣️ Todas as Compras Verbais Registradas

DataUnidade Gestora FornecedorTipo Serviço ValorLimite LegalSituação
Base legal: Art. 95 Lei 14.133/2021 + Decreto 12.807/2025 — Limite compra verbal: R$ 13.098,41 | Limite dispensa serviços: R$ 65.492,11 | Limite dispensa obras: R$ 130.984,20

⚡ Tombamento Patrimonial Pendente

Registros com Material Permanente marcado como "Sim" ou objeto identificado como bem durável (veículo, equipamento, mobiliário) sem número de tombamento registrado. Base: IN 003/2024 · Lei 4.320/1964, art. 94 · Manual de Gestão Patrimonial.
⚙️ Biblioteca de Regras por Objeto

Carregando...

💡 Recomendações do Controle Interno
📄 Emissão de Relatórios

📊 Relatório Mensal de Pagamentos

Consolidado de todos os pagamentos do período, por secretaria e tipo de serviço.

✂️ Relatório de Análise de Fracionamento

Identifica possíveis fracionamentos de compras conforme Lei 14.133/2021, art. 8º, §1º.

❌ Relatório de Pareceres Contrários

Lista todos os processos com parecer contrário, devolução ou correção pendente.

🏢 Relatório por Fornecedor

Consolida todos os pagamentos por fornecedor, identificando concentração de gastos.

⚠️ Relatório de Conformidade (Lei 14.133/2021)

Verifica conformidade dos processos com os limites de dispensa e exigências legais.

⚙️ Configurações

🔗 Integração com Google Sheets

A URL do backend deste município é definida pelo Administrador da Central. Para trocar, use o painel Admin da plataforma.

📐 Limites de Alerta

Fonte: Decreto 12.807/2025 (vigente a partir de 01/01/2026) + Lei 14.133/2021

🔗 Decreto 12.807/2025 — Planalto

☁️ Importar da Planilha Google Sheets

Busca todos os registros existentes na planilha e carrega no sistema.

📦 Backup de Dados

0 registros armazenados localmente.

📋 Como conectar ao Google Sheets

⚙️ URL ativa: ✅ Configurada e funcionando

Código atual do Apps Script (cole e reimplante se necessário):

// CI Digital — Controle Interno Municipal v4.0
// Planilha: 1CmeEPgTVc41rPM29udOAB7p5KBcGl_V8VtwCehYwms8 | GID: 598072602

var SHEET_ID  = "1CmeEPgTVc41rPM29udOAB7p5KBcGl_V8VtwCehYwms8";
var SHEET_GID = 598072602;
var TAMANHO_PAGINA = 500; // registros por página
var SENHA_MASTER  = 'CI@Belmonte2026'; // ⚠️ TROQUE AQUI para mudar a senha master

function verificarMaster(senha) {
  return String(senha||'') === SENHA_MASTER;
}

// Aba de Log de Auditoria — cria com cabeçalhos se não existir
function getAbaLog() {
  var ss  = SpreadsheetApp.openById(SHEET_ID);
  var aba = ss.getSheetByName('Log');
  if (!aba) {
    aba = ss.insertSheet('Log');
    aba.getRange(1, 1, 1, 7).setValues([[
      'TIMESTAMP','USUARIO','CARGO','PERFIL','ACAO','ALVO','DETALHE'
    ]]);
    aba.setFrozenRows(1);
    aba.getRange(1, 1, 1, 7).setFontWeight('bold').setBackground('#1e3a5f').setFontColor('#fff');
    aba.setColumnWidth(1, 150); aba.setColumnWidth(2, 200);
    aba.setColumnWidth(3, 160); aba.setColumnWidth(4, 90);
    aba.setColumnWidth(5, 180); aba.setColumnWidth(6, 240);
    aba.setColumnWidth(7, 400);
  }
  return aba;
}

function gravarLog(usuario, cargo, perfil, acao, alvo, detalhe) {
  try {
    var aba = getAbaLog();
    aba.appendRow([new Date(), usuario||'—', cargo||'—', perfil||'—',
                   acao||'—', alvo||'', detalhe||'']);
  } catch(err) {
    // Nunca quebrar a operação principal por falha de log
  }
}

// ─── USUÁRIOS (autenticação + autorização) ──────────────────
var SESSAO_DIAS = 30; // duração da sessão em dias

function getAbaUsuarios() {
  var ss  = SpreadsheetApp.openById(SHEET_ID);
  var aba = ss.getSheetByName('Usuarios');
  if (!aba) {
    aba = ss.insertSheet('Usuarios');
    aba.getRange(1, 1, 1, 15).setValues([[
      'EMAIL','NOME','CARGO','SETOR','TELEFONE','SENHA_HASH','PERFIL','STATUS',
      'CRIADO_EM','APROVADO_POR','APROVADO_EM','OBSERVACAO','TOKEN','TOKEN_EXPIRA','PRE_AUTORIZADO_POR'
    ]]);
    aba.setFrozenRows(1);
    aba.getRange(1, 1, 1, 15).setFontWeight('bold').setBackground('#1e3a5f').setFontColor('#fff');
  }
  return aba;
}

function normEmail(e) { return String(e||'').toLowerCase().trim(); }

// Retorna { linha: n, dados: [] } ou null
function buscarUsuarioPorEmail(email) {
  email = normEmail(email);
  if (!email) return null;
  var aba = getAbaUsuarios();
  var dados = aba.getDataRange().getValues();
  for (var i = 1; i < dados.length; i++) {
    if (normEmail(dados[i][0]) === email) return { linha: i+1, dados: dados[i] };
  }
  return null;
}

// Converte linha [] em objeto sem expor senha/token
function usuarioPublico(l) {
  return {
    email: String(l[0]||''), nome: String(l[1]||''), cargo: String(l[2]||''),
    setor: String(l[3]||''), telefone: String(l[4]||''), perfil: String(l[6]||''),
    status: String(l[7]||''), criadoEm: l[8] instanceof Date ? l[8].toISOString() : String(l[8]||''),
    aprovadoPor: String(l[9]||''), aprovadoEm: l[10] instanceof Date ? l[10].toISOString() : String(l[10]||''),
    observacao: String(l[11]||'')
  };
}

function gerarToken() {
  return Utilities.getUuid().replace(/-/g,'') + Utilities.getUuid().replace(/-/g,'');
}

// Valida token e retorna usuário (ou null). Atualiza expiração deslizante se válido.
function validarToken(token) {
  if (!token) return null;
  var aba = getAbaUsuarios();
  var dados = aba.getDataRange().getValues();
  for (var i = 1; i < dados.length; i++) {
    if (String(dados[i][12]||'') === String(token)) {
      var exp = dados[i][13];
      var expDt = exp instanceof Date ? exp : new Date(exp);
      if (!exp || expDt < new Date()) return null;
      if (String(dados[i][7]) !== 'APROVADO') return null;
      return { linha: i+1, dados: dados[i] };
    }
  }
  return null;
}

// Guard de ações admin: exige token válido de usuário com perfil admin
function exigirAdmin(params) {
  var token = params.token || '';
  var u = validarToken(token);
  if (!u) return null;
  if (String(u.dados[6]) !== 'admin') return null;
  return u;
}

function getSheet() {
  var ss = SpreadsheetApp.openById(SHEET_ID);
  return ss.getSheets().filter(function(s) {
    return s.getSheetId() == SHEET_GID;
  })[0] || ss.getSheets()[0];
}

// Retorna a aba Fornecedores, criando com cabeçalhos se não existir
function getAbaFornecedores() {
  var ss  = SpreadsheetApp.openById(SHEET_ID);
  var aba = ss.getSheetByName('Fornecedores');
  if (!aba) {
    aba = ss.insertSheet('Fornecedores');
    aba.getRange(1, 1, 1, 18).setValues([[
      'DOCUMENTO','TIPO','RAZAO_SOCIAL','NOME_FANTASIA','CEP','LOGRADOURO','NUMERO',
      'BAIRRO','MUNICIPIO','UF','TELEFONE','EMAIL','ATIVIDADE_PRINCIPAL','SITUACAO',
      'PORTE','OBSERVACOES','CRIADO_EM','ATUALIZADO_EM'
    ]]);
    aba.setFrozenRows(1);
    aba.getRange(1, 1, 1, 18).setFontWeight('bold').setBackground('#1e3a5f').setFontColor('#fff');
  }
  return aba;
}

// Formata data para DD/MM/YYYY
function fmtData(val) {
  if (!val) return '';
  if (val instanceof Date) {
    var d = val.getDate(), m = val.getMonth()+1, y = val.getFullYear();
    if (y < 2000 || y > 2100) return '';
    return (d<10?'0'+d:d)+'/'+(m<10?'0'+m:m)+'/'+y;
  }
  return String(val);
}

// Formata valor numérico
function fmtValor(val) {
  if (val === '' || val === null || val === undefined) return '0';
  if (typeof val === 'number') return val.toFixed(2);
  return String(val).replace(',','.');
}

function doGet(e) {
  var params = e ? e.parameter : {};
  var acao   = params.action || 'status';

  // ── AUTH: verifica se já existe algum admin (bootstrap) ───
  if (acao === 'verificarBootstrap') {
    try {
      var aba = getAbaUsuarios();
      var dados = aba.getDataRange().getValues();
      var existe = false;
      for (var i = 1; i < dados.length; i++) {
        if (String(dados[i][6]) === 'admin' && String(dados[i][7]) === 'APROVADO') {
          existe = true; break;
        }
      }
      return resposta({ status:'ok', adminExiste: existe });
    } catch(err) {
      return resposta({ status:'erro', msg: err.message });
    }
  }

  // ── AUTH: estado de um e-mail (pra login inteligente) ─────
  if (acao === 'verificarEmail') {
    var u = buscarUsuarioPorEmail(params.email);
    if (!u) return resposta({ status:'ok', estado:'nao_existe' });
    return resposta({ status:'ok', estado: String(u.dados[7]||'').toLowerCase(),
                      temSenha: !!u.dados[5], nome: String(u.dados[1]||'') });
  }

  // ── AUTH: valida token de sessão e devolve usuário ────────
  if (acao === 'validarSessao') {
    var u = validarToken(params.token);
    if (!u) return resposta({ status:'ok', valido:false });
    // renova expiração deslizante (+30 dias)
    var nova = new Date(); nova.setDate(nova.getDate() + SESSAO_DIAS);
    getAbaUsuarios().getRange(u.linha, 14).setValue(nova);
    return resposta({ status:'ok', valido:true, usuario: usuarioPublico(u.dados) });
  }

  // ── AUTH: logout (limpa token) ────────────────────────────
  if (acao === 'logout') {
    var u = validarToken(params.token);
    if (u) {
      getAbaUsuarios().getRange(u.linha, 13, 1, 2).setValues([['','']]);
      gravarLog(u.dados[0], u.dados[2], u.dados[6], 'LOGOUT', '', '');
    }
    return resposta({ status:'ok' });
  }

  // ── ADMIN: listar usuários (todos) ────────────────────────
  if (acao === 'listarUsuarios') {
    var a = exigirAdmin(params);
    if (!a) return resposta({ status:'erro', msg:'Acesso negado' });
    var aba = getAbaUsuarios();
    var dados = aba.getDataRange().getValues();
    var lista = [];
    for (var i = 1; i < dados.length; i++) {
      if (!dados[i][0]) continue;
      lista.push(usuarioPublico(dados[i]));
    }
    return resposta({ status:'ok', total: lista.length, dados: lista });
  }

  // ── Validar senha master ──────────────────────────────────
  if (acao === 'verificarMaster') {
    return resposta({ status: 'ok', valido: verificarMaster(params.senha) });
  }

  // ── Listar log de auditoria (protegido pela senha master) ──
  if (acao === 'listarLog') {
    if (!verificarMaster(params.masterKey))
      return resposta({ status:'erro', msg:'Autorização negada — senha master necessária' });
    try {
      var aba   = getAbaLog();
      var dados = aba.getDataRange().getValues();
      var lista = [];
      // mais recentes primeiro; limita últimas 500
      var ini = Math.max(1, dados.length - 500);
      for (var i = dados.length - 1; i >= ini; i--) {
        var l = dados[i];
        if (!l[0]) continue;
        lista.push({
          timestamp: l[0] instanceof Date ? l[0].toISOString() : String(l[0]||''),
          usuario:   String(l[1]||''), cargo: String(l[2]||''),
          perfil:    String(l[3]||''), acao:  String(l[4]||''),
          alvo:      String(l[5]||''), detalhe: String(l[6]||'')
        });
      }
      return resposta({ status:'ok', total: lista.length, dados: lista });
    } catch(err) {
      return resposta({ status:'erro', msg: err.message });
    }
  }

  // ── Listar com paginação ──────────────────────────────────
  if (acao === 'listar') {
    var pagina = parseInt(params.pagina || '1');
    var sheet  = getSheet();
    var ultima = sheet.getLastRow();
    var total  = Math.max(0, ultima - 1); // descontar cabeçalho
    var inicio = 1 + (pagina - 1) * TAMANHO_PAGINA; // linha da planilha (1-based, após header)
    var fim    = Math.min(inicio + TAMANHO_PAGINA - 1, total);

    if (inicio > total) {
      return resposta({ status: 'ok', total: total, pagina: pagina,
                        totalPaginas: Math.ceil(total/TAMANHO_PAGINA), dados: [] });
    }

    // getRange(linha, coluna, numLinhas, numColunas)
    var numCols  = sheet.getLastColumn();
    var range    = sheet.getRange(inicio + 1, 1, fim - inicio + 1, numCols);
    var valores  = range.getValues();
    var registros = [];

    for (var i = 0; i < valores.length; i++) {
      var l = valores[i];
      var linhaVazia = true;
      for (var k = 0; k < l.length; k++) {
        if (l[k] !== '' && l[k] !== null && l[k] !== undefined) { linhaVazia = false; break; }
      }
      if (linhaVazia) continue;
      var reg = {};
      reg._linha = inicio + i + 1; // número da linha na planilha — identidade única
      for (var j = 0; j < l.length; j++) {
        if (l[j] instanceof Date) {
          reg[j] = fmtData(l[j]);
        } else if (j === 13) {
          reg[j] = fmtValor(l[j]);
        } else {
          reg[j] = l[j] !== undefined && l[j] !== null ? String(l[j]) : '';
        }
      }
      registros.push(reg);
    }

    return resposta({
      status: 'ok',
      total: total,
      pagina: pagina,
      totalPaginas: Math.ceil(total / TAMANHO_PAGINA),
      dados: registros
    });
  }

  // ── Listar aba Cadastro (domínios para autocomplete) ──────
  if (acao === 'listarCadastro') {
    try {
      var ss    = SpreadsheetApp.openById(SHEET_ID);
      var aba   = ss.getSheetByName('Cadastro');
      if (!aba) return resposta({ status: 'erro', msg: 'Aba "Cadastro" não encontrada.' });
      var dados = aba.getDataRange().getValues();
      if (dados.length < 2) return resposta({ status: 'ok', unidades: [], tipos: [], pareceres: [], instrucoes: [], fornecedores: [] });
      var cols = { unidades: [], tipos: [], pareceres: [], instrucoes: [], fornecedores: [] };
      var keys = ['unidades','tipos','pareceres','instrucoes','fornecedores'];
      for (var i = 1; i < dados.length; i++) {
        for (var j = 0; j < 5; j++) {
          var v = dados[i][j];
          if (v !== '' && v !== null && v !== undefined) {
            var s = String(v).trim();
            if (s) cols[keys[j]].push(s);
          }
        }
      }
      // remove duplicatas preservando ordem
      keys.forEach(function(k) {
        var seen = {};
        cols[k] = cols[k].filter(function(x) { return seen[x] ? false : (seen[x] = true); });
      });
      return resposta({ status: 'ok', unidades: cols.unidades, tipos: cols.tipos,
                        pareceres: cols.pareceres, instrucoes: cols.instrucoes,
                        fornecedores: cols.fornecedores });
    } catch(err) {
      return resposta({ status: 'erro', msg: err.message });
    }
  }

  // ── Listar fornecedores cadastrados ───────────────────────
  if (acao === 'listarFornecedores') {
    try {
      var aba   = getAbaFornecedores();
      var dados = aba.getDataRange().getValues();
      var lista = [];
      for (var i = 1; i < dados.length; i++) {
        var l = dados[i];
        if (!l[0]) continue;
        lista.push({
          documento:    String(l[0]),
          tipo:         String(l[1]||''),
          razaoSocial:  String(l[2]||''),
          nomeFantasia: String(l[3]||''),
          cep:          String(l[4]||''),
          logradouro:   String(l[5]||''),
          numero:       String(l[6]||''),
          bairro:       String(l[7]||''),
          municipio:    String(l[8]||''),
          uf:           String(l[9]||''),
          telefone:     String(l[10]||''),
          email:        String(l[11]||''),
          atividade:    String(l[12]||''),
          situacao:     String(l[13]||''),
          porte:        String(l[14]||''),
          observacoes:  String(l[15]||''),
          criadoEm:     fmtData(l[16]),
          atualizadoEm: fmtData(l[17])
        });
      }
      return resposta({ status: 'ok', total: lista.length, dados: lista });
    } catch(err) {
      return resposta({ status: 'erro', msg: err.message });
    }
  }

  // ── Excluir fornecedor por documento (exige senha master) ──
  if (acao === 'excluirFornecedor') {
    if (!verificarMaster(params.masterKey))
      return resposta({ status:'erro', msg:'Autorização negada — senha master necessária' });
    try {
      var doc = String(params.documento||'').replace(/\D/g,'');
      if (!doc) return resposta({ status: 'erro', msg: 'Documento obrigatório' });
      var aba   = getAbaFornecedores();
      var dados = aba.getDataRange().getValues();
      for (var i = 1; i < dados.length; i++) {
        if (String(dados[i][0]).replace(/\D/g,'') === doc) {
          var razao = dados[i][2];
          aba.deleteRow(i+1);
          gravarLog(params.usuario, params.cargo, params.perfil,
                    'EXCLUIR_FORNECEDOR', doc, String(razao||''));
          return resposta({ status: 'ok', acao: 'excluido', linha: i+1 });
        }
      }
      return resposta({ status: 'nao_encontrado' });
    } catch(err) {
      return resposta({ status: 'erro', msg: err.message });
    }
  }

  // ── Adicionar cabeçalhos das novas colunas ────────────────
  if (acao === 'addColunas') {
    var sheet  = getSheet();
    var ultima = sheet.getLastColumn();
    var header = sheet.getRange(1, 1, 1, ultima).getValues()[0];
    var novos  = ['MODALIDADE','COMPRA VERBAL','DOTAÇÃO','CNPJ/CPF',
                  'TEVE LICITAÇÃO','CONTRATO VIGENTE'];
    var adicionados = 0;
    novos.forEach(function(nome) {
      if (header.indexOf(nome) === -1) {
        ultima++;
        sheet.getRange(1, ultima).setValue(nome);
        adicionados++;
      }
    });
    return resposta({ status: 'ok', adicionados: adicionados });
  }

  // ── Excluir registro por número (exige senha master) ──────
  if (acao === 'excluirRegistro') {
    if (!verificarMaster(params.masterKey))
      return resposta({ status:'erro', msg:'Autorização negada — senha master necessária' });
    try {
      var sheet = getSheet();
      var numero = parseInt(params.numero||'0');
      var dados  = sheet.getDataRange().getValues();
      for (var i = 1; i < dados.length; i++) {
        if (parseInt(dados[i][0]) === numero) {
          var fornecedor = dados[i][9];
          sheet.deleteRow(i+1);
          gravarLog(params.usuario, params.cargo, params.perfil,
                    'EXCLUIR_REGISTRO', 'Nº ' + numero, String(fornecedor||''));
          return resposta({ status:'ok', linha: i+1 });
        }
      }
      return resposta({ status: 'nao_encontrado' });
    } catch(err) {
      return resposta({ status: 'erro', msg: err.message });
    }
  }

  // ── Atualizar registro existente ──────────────────────────
  if (acao === 'atualizar') {
    try {
      var sheet  = getSheet();
      var numero = parseInt(params.numero || '0');
      var col    = parseInt(params.coluna || '0');
      var valor  = params.valor || '';
      var dados  = sheet.getDataRange().getValues();
      for (var i = 1; i < dados.length; i++) {
        if (parseInt(dados[i][0]) === numero) {
          sheet.getRange(i+1, col+1).setValue(valor);
          return resposta({ status: 'ok', linha: i+1 });
        }
      }
      return resposta({ status: 'nao_encontrado' });
    } catch(err) {
      return resposta({ status: 'erro', msg: err.message });
    }
  }

  return resposta({ status: 'ativo', sistema: 'CI Digital', versao: '4.0' });
}

// ── POST: grava novo registro ─────────────────────────────────
function doPost(e) {
  try {
    var data = JSON.parse(e.postData.contents);

    // ─── AUTH ─────────────────────────────────────────────────

    // Bootstrap do primeiro admin (senha master obrigatória)
    if (data.bootstrap) {
      if (!verificarMaster(data.senhaMaster))
        return resposta({ status:'erro', msg:'Senha master inválida' });
      var email = normEmail(data.email);
      if (!email || !data.nome || !data.senhaHash)
        return resposta({ status:'erro', msg:'Dados incompletos' });
      if (buscarUsuarioPorEmail(email))
        return resposta({ status:'erro', msg:'E-mail já cadastrado' });
      var aba = getAbaUsuarios();
      var token = gerarToken();
      var expira = new Date(); expira.setDate(expira.getDate() + SESSAO_DIAS);
      var agora = new Date();
      aba.appendRow([email, data.nome, data.cargo||'Controlador Interno', data.setor||'',
                     data.telefone||'', data.senhaHash, 'admin', 'APROVADO',
                     agora, 'BOOTSTRAP', agora, 'Admin inicial via senha master',
                     token, expira, '']);
      gravarLog(email, data.cargo||'—', 'admin', 'BOOTSTRAP_ADMIN', email, data.nome);
      return resposta({ status:'ok', token: token,
                        usuario: { email:email, nome:data.nome, cargo:data.cargo||'Controlador Interno',
                                   perfil:'admin', status:'APROVADO' } });
    }

    // Cadastro público de usuário
    if (data.cadastrar) {
      var email = normEmail(data.email);
      if (!email || !data.nome || !data.senhaHash)
        return resposta({ status:'erro', msg:'Preencha e-mail, nome e senha' });
      var existente = buscarUsuarioPorEmail(email);
      // Se já existe e está PRE_AUTORIZADO, o usuário só define a senha e é aprovado
      if (existente && String(existente.dados[7]) === 'PRE_AUTORIZADO') {
        var abaU = getAbaUsuarios();
        var token = gerarToken();
        var exp = new Date(); exp.setDate(exp.getDate() + SESSAO_DIAS);
        var agora = new Date();
        abaU.getRange(existente.linha, 2).setValue(data.nome);
        if (data.cargo)    abaU.getRange(existente.linha, 3).setValue(data.cargo);
        if (data.setor)    abaU.getRange(existente.linha, 4).setValue(data.setor);
        if (data.telefone) abaU.getRange(existente.linha, 5).setValue(data.telefone);
        abaU.getRange(existente.linha, 6).setValue(data.senhaHash);
        abaU.getRange(existente.linha, 8).setValue('APROVADO');
        abaU.getRange(existente.linha, 11).setValue(agora);
        abaU.getRange(existente.linha, 13).setValue(token);
        abaU.getRange(existente.linha, 14).setValue(exp);
        gravarLog(email, data.cargo||'', existente.dados[6], 'PRIMEIRO_ACESSO', email, data.nome);
        var atualizado = abaU.getRange(existente.linha, 1, 1, 15).getValues()[0];
        return resposta({ status:'ok', token: token, usuario: usuarioPublico(atualizado) });
      }
      if (existente)
        return resposta({ status:'erro', msg:'E-mail já cadastrado' });
      var aba = getAbaUsuarios();
      aba.appendRow([email, data.nome, data.cargo||'', data.setor||'', data.telefone||'',
                     data.senhaHash, 'leitor', 'PENDENTE', new Date(), '', '',
                     data.observacao||'', '', '', '']);
      gravarLog(email, data.cargo||'—', '—', 'CADASTRO_SOLICITADO', email, data.nome);
      return resposta({ status:'ok', pendente: true });
    }

    // Login
    if (data.login) {
      var email = normEmail(data.email);
      var u = buscarUsuarioPorEmail(email);
      if (!u) return resposta({ status:'erro', msg:'E-mail não encontrado', codigo:'nao_existe' });
      var st = String(u.dados[7]||'');
      if (st === 'PENDENTE')       return resposta({ status:'erro', msg:'Seu cadastro aguarda aprovação do administrador', codigo:'pendente' });
      if (st === 'PRE_AUTORIZADO') return resposta({ status:'erro', msg:'Primeiro acesso — use "Primeiro acesso" para definir sua senha', codigo:'pre_autorizado' });
      if (st === 'REJEITADO')      return resposta({ status:'erro', msg:'Cadastro rejeitado. Contate o administrador.', codigo:'rejeitado' });
      if (st === 'SUSPENSO')       return resposta({ status:'erro', msg:'Conta suspensa. Contate o administrador.', codigo:'suspenso' });
      if (st !== 'APROVADO')       return resposta({ status:'erro', msg:'Status inválido', codigo:'desconhecido' });
      if (String(u.dados[5]||'') !== String(data.senhaHash||''))
        return resposta({ status:'erro', msg:'Senha incorreta', codigo:'senha_incorreta' });
      var token = gerarToken();
      var exp = new Date(); exp.setDate(exp.getDate() + SESSAO_DIAS);
      var aba = getAbaUsuarios();
      aba.getRange(u.linha, 13).setValue(token);
      aba.getRange(u.linha, 14).setValue(exp);
      gravarLog(email, u.dados[2], u.dados[6], 'LOGIN', email, '');
      return resposta({ status:'ok', token: token, usuario: usuarioPublico(u.dados) });
    }

    // ── ADMIN: aprovar usuário pendente ─────────────────────
    if (data.aprovarUsuario) {
      var adm = exigirAdmin(data);
      if (!adm) return resposta({ status:'erro', msg:'Acesso negado' });
      var u = buscarUsuarioPorEmail(data.email);
      if (!u) return resposta({ status:'erro', msg:'Usuário não encontrado' });
      var aba = getAbaUsuarios();
      aba.getRange(u.linha, 7).setValue(data.perfil || u.dados[6] || 'leitor');
      aba.getRange(u.linha, 8).setValue('APROVADO');
      aba.getRange(u.linha, 10).setValue(adm.dados[0]);
      aba.getRange(u.linha, 11).setValue(new Date());
      gravarLog(adm.dados[0], adm.dados[2], 'admin', 'APROVAR_USUARIO', data.email,
                'perfil=' + (data.perfil||u.dados[6]));
      return resposta({ status:'ok' });
    }

    // ── ADMIN: rejeitar usuário ─────────────────────────────
    if (data.rejeitarUsuario) {
      var adm = exigirAdmin(data);
      if (!adm) return resposta({ status:'erro', msg:'Acesso negado' });
      var u = buscarUsuarioPorEmail(data.email);
      if (!u) return resposta({ status:'erro', msg:'Usuário não encontrado' });
      var aba = getAbaUsuarios();
      aba.getRange(u.linha, 8).setValue('REJEITADO');
      aba.getRange(u.linha, 10).setValue(adm.dados[0]);
      aba.getRange(u.linha, 11).setValue(new Date());
      aba.getRange(u.linha, 12).setValue(data.observacao||'');
      aba.getRange(u.linha, 13, 1, 2).setValues([['','']]);
      gravarLog(adm.dados[0], adm.dados[2], 'admin', 'REJEITAR_USUARIO', data.email, data.observacao||'');
      return resposta({ status:'ok' });
    }

    // ── ADMIN: suspender / reativar ─────────────────────────
    if (data.suspenderUsuario) {
      var adm = exigirAdmin(data);
      if (!adm) return resposta({ status:'erro', msg:'Acesso negado' });
      var u = buscarUsuarioPorEmail(data.email);
      if (!u) return resposta({ status:'erro', msg:'Usuário não encontrado' });
      var aba = getAbaUsuarios();
      aba.getRange(u.linha, 8).setValue('SUSPENSO');
      aba.getRange(u.linha, 13, 1, 2).setValues([['','']]);
      gravarLog(adm.dados[0], adm.dados[2], 'admin', 'SUSPENDER_USUARIO', data.email, data.observacao||'');
      return resposta({ status:'ok' });
    }
    if (data.reativarUsuario) {
      var adm = exigirAdmin(data);
      if (!adm) return resposta({ status:'erro', msg:'Acesso negado' });
      var u = buscarUsuarioPorEmail(data.email);
      if (!u) return resposta({ status:'erro', msg:'Usuário não encontrado' });
      getAbaUsuarios().getRange(u.linha, 8).setValue('APROVADO');
      gravarLog(adm.dados[0], adm.dados[2], 'admin', 'REATIVAR_USUARIO', data.email, '');
      return resposta({ status:'ok' });
    }

    // ── ADMIN: editar perfil ────────────────────────────────
    if (data.editarPerfilUsuario) {
      var adm = exigirAdmin(data);
      if (!adm) return resposta({ status:'erro', msg:'Acesso negado' });
      var u = buscarUsuarioPorEmail(data.email);
      if (!u) return resposta({ status:'erro', msg:'Usuário não encontrado' });
      getAbaUsuarios().getRange(u.linha, 7).setValue(data.perfil||'leitor');
      gravarLog(adm.dados[0], adm.dados[2], 'admin', 'EDITAR_PERFIL', data.email,
                'novo=' + (data.perfil||'leitor'));
      return resposta({ status:'ok' });
    }

    // ── ADMIN: pré-autorizar em lote ────────────────────────
    // data.lista = [ { email, perfil, nome? }, ... ]
    if (data.preAutorizarLote) {
      var adm = exigirAdmin(data);
      if (!adm) return resposta({ status:'erro', msg:'Acesso negado' });
      var lista = Array.isArray(data.lista) ? data.lista : [];
      var aba = getAbaUsuarios();
      var inseridos = 0, atualizados = 0, erros = 0;
      lista.forEach(function(it) {
        try {
          var email = normEmail(it.email);
          if (!email) { erros++; return; }
          var existente = buscarUsuarioPorEmail(email);
          var perfil = it.perfil || 'leitor';
          if (existente) {
            // só atualiza perfil se ainda está pré-autorizado/pendente
            var st = String(existente.dados[7]||'');
            if (st === 'PRE_AUTORIZADO' || st === 'PENDENTE') {
              aba.getRange(existente.linha, 7).setValue(perfil);
              aba.getRange(existente.linha, 8).setValue('PRE_AUTORIZADO');
              aba.getRange(existente.linha, 15).setValue(adm.dados[0]);
              atualizados++;
            }
          } else {
            aba.appendRow([email, it.nome||'', '', '', '', '', perfil, 'PRE_AUTORIZADO',
                           new Date(), '', '', '', '', '', adm.dados[0]]);
            inseridos++;
          }
        } catch(e) { erros++; }
      });
      gravarLog(adm.dados[0], adm.dados[2], 'admin', 'PRE_AUTORIZAR_LOTE',
                inseridos + ' novos / ' + atualizados + ' atualizados',
                lista.length + ' entradas');
      return resposta({ status:'ok', inseridos: inseridos, atualizados: atualizados, erros: erros });
    }

    // ── Salvar/atualizar fornecedor ─────────────────────────
    if (data.salvarFornecedor) {
      var aba   = getAbaFornecedores();
      var todos = aba.getDataRange().getValues();
      var doc   = String(data.documento||'').replace(/\D/g,'');
      if (!doc) return resposta({ status:'erro', msg:'Documento obrigatório' });
      var agora = new Date();
      var linha = [
        doc, data.tipo||'', data.razaoSocial||'', data.nomeFantasia||'',
        data.cep||'', data.logradouro||'', data.numero||'', data.bairro||'',
        data.municipio||'', data.uf||'', data.telefone||'', data.email||'',
        data.atividade||'', data.situacao||'', data.porte||'',
        data.observacoes||'', agora, agora
      ];
      for (var k = 1; k < todos.length; k++) {
        if (String(todos[k][0]).replace(/\D/g,'') === doc) {
          linha[16] = todos[k][16] || agora; // preserva criadoEm
          aba.getRange(k+1, 1, 1, 18).setValues([linha]);
          gravarLog(data.usuario, data.cargo, data.perfil,
                    'EDITAR_FORNECEDOR', doc, data.razaoSocial||'');
          return resposta({ status:'ok', acao:'atualizado', linha:k+1 });
        }
      }
      aba.appendRow(linha);
      gravarLog(data.usuario, data.cargo, data.perfil,
                'SALVAR_FORNECEDOR', doc, data.razaoSocial||'');
      return resposta({ status:'ok', acao:'inserido' });
    }

    var sheet = getSheet();

    // Se for edição (tem campo 'editar': true e 'numeroLinha'), atualiza a linha
    if (data.editar && data.numero) {
      var todos = sheet.getDataRange().getValues();
      for (var i = 1; i < todos.length; i++) {
        if (String(todos[i][0]) === String(data.numero)) {
          sheet.getRange(i+1, 1, 1, 23).setValues([[
            data.numero, data.data, data.unidade, data.tipo, data.parecer,
            data.descricao, data.numeroCi, data.dataCip, data.recebidoEm,
            data.fornecedor, data.tipoServico, data.mesRef, data.nfe,
            parseFloat(data.valor)||0, data.enviadoCont, data.recebidoPor, data.obs,
            data.modalidade||'', data.compraVerbal||'', data.dotacao||'', data.cnpj||'',
            data.teveLicitacao||'', data.contratoVigente||''
          ]]);
          gravarLog(data.usuario, data.cargo, data.perfil,
                    'EDITAR_REGISTRO', 'Nº ' + data.numero, data.fornecedor||'');
          return resposta({ status: 'ok', acao: 'atualizado', linha: i+1 });
        }
      }
    }

    // Novo registro
    sheet.appendRow([
      data.numero, data.data, data.unidade, data.tipo, data.parecer,
      data.descricao, data.numeroCi, data.dataCip, data.recebidoEm,
      data.fornecedor, data.tipoServico, data.mesRef, data.nfe,
      parseFloat(data.valor)||0, data.enviadoCont, data.recebidoPor, data.obs,
      data.modalidade||'', data.compraVerbal||'', data.dotacao||'', data.cnpj||'',
      data.teveLicitacao||'', data.contratoVigente||''
    ]);
    gravarLog(data.usuario, data.cargo, data.perfil,
              'SALVAR_REGISTRO', 'Nº ' + data.numero, data.fornecedor||'');
    return resposta({ status: 'ok', acao: 'inserido', registro: data.numero });
  } catch(err) {
    return resposta({ status: 'erro', msg: err.message });
  }
}

function resposta(obj) {
  return ContentService
    .createTextOutput(JSON.stringify(obj))
    .setMimeType(ContentService.MimeType.JSON);
}

// ═══ MANUTENÇÃO (rodar no editor) ═══
// Mostra quantos registros tem por ano (rode antes da limpeza)
function contarAno2025() {
  var sheet = getSheet();
  var dados = sheet.getDataRange().getValues();
  var c2025=0, c2026=0, cOutros=0, cSemData=0;
  for (var i = 1; i < dados.length; i++) {
    var v = dados[i][1], ano = null;
    if (!v) { cSemData++; continue; }
    if (v instanceof Date) ano = v.getFullYear();
    else {
      var m1 = String(v).trim().match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
      var m2 = String(v).trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
      if (m1) ano = parseInt(m1[3]); else if (m2) ano = parseInt(m2[1]);
    }
    if (ano===2025) c2025++; else if (ano===2026) c2026++;
    else cOutros++;
  }
  var msg = 'Total: ' + (dados.length-1) + ' | 2025: '+c2025+' | 2026: '+c2026+' | Outros: '+cOutros+' | Sem data: '+cSemData;
  Logger.log(msg); return msg;
}

// Apaga todas as linhas de 2025 na aba principal. IRREVERSÍVEL — faça backup antes!
function limparAno2025() {
  var sheet = getSheet();
  var dados = sheet.getDataRange().getValues();
  var linhasRemover = [];
  for (var i = 1; i < dados.length; i++) {
    var v = dados[i][1], ano = null;
    if (!v) continue;
    if (v instanceof Date) ano = v.getFullYear();
    else {
      var m1 = String(v).trim().match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
      var m2 = String(v).trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
      if (m1) ano = parseInt(m1[3]); else if (m2) ano = parseInt(m2[1]);
    }
    if (ano === 2025) linhasRemover.push(i+1);
  }
  for (var j = linhasRemover.length - 1; j >= 0; j--) sheet.deleteRow(linhasRemover[j]);
  var msg = 'Removidas ' + linhasRemover.length + ' linhas de 2025.';
  Logger.log(msg);
  gravarLog('SISTEMA', '—', 'admin', 'LIMPEZA_ANO', 'ano=2025', linhasRemover.length + ' linhas');
  return msg;
}

Passos: Apps Script → cole o código → Implantar → Gerenciar → Editar → "Qualquer pessoa" → Implantar

🔔 Solicitações de Acesso

QuandoE-mailNomeCargoSetor TelefoneAção

👤 Gestão de Usuários

StatusE-mailNomeCargoPerfil Criado emAções

⚡ Pré-autorizar Usuários em Lote

Cole abaixo uma lista de e-mails já autorizados. Cada linha no formato email, perfil, nome (opcional). Perfis válidos: leitor, editor, admin.

Como funciona: os e-mails listados entram com status PRE_AUTORIZADO. No próximo acesso de cada um, basta usar a aba "🌱 1º Acesso" na tela de login para criar a senha — entrarão no sistema imediatamente sem precisar de nova aprovação.

🎨 Identidade Visual do Município

Personalize o sistema com a identidade do seu município. Essas informações aparecem no sidebar, nos cabeçalhos de impressão e nos relatórios em PDF.

Cole uma URL de imagem pública. Dica: suba no Imgur, clique com botão direito na imagem e "Copiar endereço da imagem".
PRÉ-VISUALIZAÇÃO — CABEÇALHO DE PDF

📋 Modelos de Parecer

TítuloTipoParecerConteúdoAções
💡 Variáveis disponíveis: ao criar um modelo, use {{fornecedor}}, {{valor}}, {{numero}}, {{data}}, {{unidade}}, {{tipoServico}}, {{recebidoPor}}, {{descricao}}, {{modalidade}} — serão substituídos pelos dados do registro ao aplicar.

📜 Normas, Instruções Normativas e Resoluções

Tipo Número Data Ementa Status Ações
📘 Dica: use a Biblioteca de Modelos para começar a partir de uma IN pronta já fundamentada em lei. Para cadastrar normas já vigentes no município, digite o número original (ex: 007/2019) — só use a sugestão automática para normas novas.

📊 Pareceres por Conclusão

ConclusãoQtde%

🏛️ Conformidade por Secretaria

UnidadeTotalCom Risco% ConformeValor
ℹ️ Sobre os indicadores: todas as métricas são calculadas em tempo real a partir dos dados da planilha. Filtre por ano para análises específicas. O PDF serve como relatório para apresentação ao Prefeito, Câmara ou TCE.

📅 Ações do Plano Anual de Atividades

# Eixo Temático Ação Período Responsável Status % Ações
📘 Sobre o PAACI: Plano Anual de Atividades do Controle Interno, conforme IN 001/2024 (AMUPE). Elabore até 31 de dezembro para vigência no exercício seguinte. Use o PDF Quadrimestral para entregas ao Prefeito.

🔍 Missões de Auditoria Interna

Tipo Título / Objeto Unidade Período Fase Status % Ações
📘 O que é uma Missão de Auditoria: diferente do parecer de pagamento, é uma auditoria formal temática com escopo, equipe, cronograma e entregas — conforme IPPF/IIA e MOT AUGE da CGU. Passa por 5 fases: APO (análise preliminar) → Planejamento → Execução → Relatório → Monitoramento.

🌡️ Heatmap (Probabilidade × Impacto)

Eixo X = Impacto (1–5) · Eixo Y = Probabilidade (1–5) · Número = qtde de riscos naquela célula

📊 Matriz de Riscos (COSO ICIF 2013)

Categoria Evento de Risco Processo/Unidade P I Inerente Controles Residual Resposta Ações

📘 Biblioteca de Roteiros de Auditoria

Scripts técnicos por objeto auditado — use como referência ao planejar uma auditoria. Cada roteiro tem questões-chave, procedimentos recomendados e técnicas de amostragem.

📄 Histórico de Pareceres Emitidos

Nº Parecer Emissão Processo Ref Unidade / Fornecedor Valor Conclusão Status Ações
📘 Fluxo correto: o registro é lançado primeiro (com parecer "Pendente de Análise"). Depois de analisar, você clica no 📄 do registro e emite o parecer formal — que aparece aqui com numeração sequencial, pronto pra mandar pra Contabilidade.

🎛️ Parametrização do Município

Edite as listas que alimentam os selects do sistema (Secretarias, Tipos de Processo, Pareceres, Modalidades, Cargos etc.). Cada município tem a sua configuração. Uma lista por linha. Os valores são gravados na aba Cadastro da sua planilha.

🏛️ Unidades Gestoras / Órgãos

📂 Tipos de Processo

✅ Tipos de Parecer

⚖️ Modalidades de Contratação

👤 Cargos / Setores

📜 Instruções Normativas

📄 Abrir planilha diretamente
💡 Dica: você também pode editar diretamente na aba Cadastro da planilha — os dois caminhos funcionam. O sistema atualiza automaticamente.

🔍 Registro de Auditoria — últimas 500 ações

QuandoUsuárioCargoPerfil AçãoAlvoDetalhe

👋 Bem-vindo ao CI Digital

O sistema de controle interno municipal da sua cidade — rápido, acessível e seguro.

🚀Primeiros passos
📝Novo registro
👥Fornecedores
⚠️Pareceres e Riscos
🔐Perfis de Acesso
Perguntas frequentes

🎯 O que é o CI Digital?

É a plataforma municipal para registrar, analisar e emitir pareceres sobre pagamentos, diárias, contratos e recomendações. Os dados são gravados em tempo real em uma planilha segura do Google, acessível pela equipe de Controle Interno de qualquer lugar.

O sistema organiza seu trabalho em três fluxos:

  • Lançamentos — onde você registra os processos recebidos
  • Análise — onde o sistema aponta riscos e recomendações automaticamente
  • Relatórios — onde você exporta tudo em CSV ou imprime

🚀 Primeiros Passos (em 3 minutos)

Faça o login. Digite seu nome e selecione seu cargo/setor. É por esse cargo que o sistema decide o que você pode fazer.
Conheça o menu lateral. Os itens são agrupados por finalidade: Painel, Lançamentos, Cadastros, Análise e Relatórios.
Observe a barra no topo. Ali aparece seu perfil (👁️ Leitor / ✏️ Editor / 🔑 Admin) e o botão 🔄 Atualizar para sincronizar com a planilha.
Dê uma olhada no Dashboard. Ele traz um resumo visual dos últimos processos, fornecedores, pareceres e valores.
💡 Dica: o sistema salva tudo automaticamente na planilha e também no seu dispositivo — funciona mesmo se a internet cair no meio do trabalho.

📝 Registrando um Novo Pagamento

Clique em + Novo Registro no topo ou no menu Lançamentos.
Preencha os campos obrigatórios (marcados com *): Unidade Gestora, Tipo de Processo, Fornecedor, Tipo de Serviço, Valor, Modalidade, Compra Verbal, Licitação, Contrato Vigente e Parecer.
Busque o fornecedor digitando. O campo Fornecedor é inteligente — ao digitar, ele mostra sugestões da base Cadastro e de Fornecedores cadastrados. Escolha um já existente para evitar duplicatas.
Se selecionar um fornecedor cadastrado, o CNPJ/CPF é preenchido automaticamente.
Clique em 💾 Salvar Registro. O sistema grava local e envia para a planilha em segundos.
⚠️ Atenção — Compra Verbal: o limite legal é R$ 13.098,41 (art. 95 Lei 14.133/2021). Acima disso, a compra é irregular e pode configurar fracionamento.

👥 Cadastrando um Fornecedor com busca na Receita

No menu, vá em Cadastros → Fornecedores e clique em + Novo Fornecedor.
Escolha o tipo: 🏢 Pessoa Jurídica ou 👤 Pessoa Física.
Para PJ: digite o CNPJ (o sistema formata automaticamente) e clique em 🔍 Buscar Receita. Razão Social, endereço, telefone, e-mail e atividade vêm direto da Receita Federal.
Para PF: digite o CPF (com validação dos dígitos) e preencha nome e endereço manualmente.
Complete os campos restantes se precisar, revise e clique 💾 Salvar Fornecedor.
💡 Benefício: todo fornecedor cadastrado aqui aparece no autocomplete da tela de Novo Registro, com seu CNPJ vinculado. Menos digitação, menos erro, mais rastreabilidade.

⚠️ Pareceres, Riscos e Recomendações

Os 6 tipos de Parecer

  • Favorável — processo regular, liberado para pagamento
  • Contrário — há impedimento legal ou documental
  • 🔄 Devolução — volta ao setor de origem para ajustes
  • ✏️ Correção — requer correção pontual
  • 🕐 Em análise — ainda sob avaliação
  • 🗂️ Arquivado — processo encerrado sem continuidade

Análise de Riscos

O menu ⚠️ Análise de Riscos aponta automaticamente processos suspeitos: compras verbais acima do limite, fracionamento de despesa, concentração em poucos fornecedores, falta de modalidade etc. Use esse painel para priorizar o trabalho.

🔐 Contas, Perfis de Acesso e Senha Master

Como obter acesso

Cada usuário tem sua própria conta com e-mail e senha. Há três formas de começar:

  1. 📝 Criar conta — preencha seus dados e a senha. Sua solicitação fica pendente até o Administrador aprovar.
  2. 🌱 Primeiro acesso — se o Administrador já te pré-autorizou em lote, use essa aba para criar sua senha e entrar imediatamente.
  3. 🔑 Entrar — e-mail e senha normais. A sessão dura 30 dias mesmo trocando de navegador.

Os três perfis

O perfil é definido pelo Administrador no momento da aprovação (independe do cargo informado):

👁️
Leitor
Visualiza dashboard, registros, análise e relatórios. Exporta CSV. Não cria nem edita nada.
✏️
Editor
Tudo do Leitor + cria e edita registros, cadastra fornecedores.
🔑
Administrador
Tudo + exclui, aprova usuários, acessa Log/Configurações, pré-autoriza em lote e executa ações com senha master.

A Senha Master

Protege ações destrutivas (excluir registro/fornecedor). Só o Admin sabe. Quando um Editor precisa executar algo assim, chama o Admin, que digita a senha no momento e autoriza — fica tudo registrado no Log.

💡 Atualizações futuras do sistema não exigem novo cadastro: sua conta vive na planilha e a sessão dura 30 dias no navegador.

❓ Perguntas Frequentes

Meus dados somem se eu fechar o navegador?

Não. Tudo fica salvo na planilha do Google e também no seu navegador. Ao reabrir, puxamos os últimos dados automaticamente.

Em quanto tempo vejo o que outro usuário lançou?

Em no máximo 1 minuto (atualização automática). Ou clique em 🔄 Atualizar no topo para forçar.

Posso usar no celular?

Sim — o sistema é web e funciona em qualquer navegador. Basta acessar a URL do sistema.

Não consigo excluir um registro. Por quê?

Excluir é uma ação protegida — o sistema pede a senha master, que só o Administrador tem. Isso evita exclusões acidentais.

Cadastrei um CNPJ mas o botão "Buscar Receita" falhou.

Confirme que o CNPJ é válido e tem 14 dígitos. Se a empresa for muito recente, pode ainda não constar na base pública da Receita.

Quero trocar a senha master. Como faço?

A senha fica no Apps Script. Só o responsável técnico edita o código e reimplanta (cerca de 5 min). Nunca compartilhe a senha por e-mail ou WhatsApp.

💡 Dicas de Produtividade

  • Use Ctrl+F na página Registros para buscar rapidamente.
  • Cadastre o fornecedor antes de lançar o registro — o CNPJ vem preenchido automaticamente depois.
  • Confira o Dashboard no começo do dia para ver pendências e riscos.
  • Exporte para CSV no fim do mês para fechar relatórios internos.
  • Se trocar de usuário no mesmo computador, clique no badge 👤 no topo direito.

🏛️ Sobre a Plataforma Multi-Município

O Controle Interno Digital é uma plataforma nacional: cada município tem seus dados em uma planilha exclusiva, isolada, sob domínio oficial controleinternodigital.com.br.

Como acessar

  • URL direta do seu município: https://[seu-slug].controleinternodigital.com.br
  • Portal público: https://controleinternodigital.com.br e clica no card do município

Vantagens

  • 🔒 Seus dados ficam apenas na planilha do seu município — nenhuma mistura entre cidades
  • ☁️ Atualizações do sistema são automáticas — zero trabalho para o município
  • 🆓 Sem custo de licença — você só paga sua própria hospedagem Google (gratuita pra 99% dos casos)
  • 📱 Funciona em qualquer dispositivo com navegador moderno
  • 🔌 Funciona offline e sincroniza quando a internet volta

💡 Recursos-Chave v4.2

  • 🎨 Identidade Visual — brasão, cores e cabeçalho personalizados pelo próprio município
  • 📄 PDFs profissionais — relatórios com cabeçalho oficial, assinatura do Controlador e rodapé
  • 💰 Comparador de Preços — histórico automático ao digitar Tipo de Serviço; alerta se valor está acima da média
  • 🔍 Consultas CEIS/CND/FGTS — botões diretos para portais oficiais de conformidade
  • 📋 Modelos de Parecer — templates com variáveis substituíveis automaticamente
  • 📜 Histórico de alterações — veja quem mudou o quê em cada registro
  • 📈 Gráfico evolutivo — comparativo ano atual vs anterior, mês a mês
  • 🔐 Sistema de contas + aprovação — cadastro, pendências e pré-autorização em lote
  • 🔍 Log de auditoria completo — rastreamento de todas as ações sensíveis
  • Sincronização em tempo real — múltiplos usuários veem mudanças em até 1 minuto

Controle Interno Digital · v4.2 · Abril/2026
Plataforma Multi-Município · controleinternodigital.com.br
Este manual pode ser impresso ou salvo em PDF pelo botão acima.