📈 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
| Nº | Data | Unidade Gestora | Fornecedor | Tipo Serviço | Valor | Parecer | Risco |
|---|
👥 Fornecedores Cadastrados
| Tipo | Documento | Razão Social / Nome | Nome Fantasia | Município/UF | Situação | Ações |
|---|
📋 Registros de Pagamento
| Nº | Data | Unidade Gestora | Fornecedor | Tipo Serviço | Modalidade | Verbal? | Licitação? | Contrato? | Valor | Parecer | Risco | Ações |
|---|
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
| Nº | Data | Unidade Gestora | Fornecedor | Tipo Serviço | Valor | Limite Legal | Situação |
|---|
⚡ Tombamento Patrimonial Pendente
Carregando...
📊 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.
🔗 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
☁️ 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
| Quando | Nome | Cargo | Setor | Telefone | Ação |
|---|
👤 Gestão de Usuários
| Status | Nome | Cargo | Perfil | Criado em | Açõ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.
🎨 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.
📋 Modelos de Parecer
| Título | Tipo | Parecer | Conteúdo | Ações |
|---|
{{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 |
|---|
007/2019) — só use a sugestão automática para normas novas.
📊 Pareceres por Conclusão
| Conclusão | Qtde | % |
|---|
🏛️ Conformidade por Secretaria
| Unidade | Total | Com Risco | % Conforme | Valor |
|---|
📅 Ações do Plano Anual de Atividades
| # | Eixo Temático | Ação | Período | Responsável | Status | % | Ações |
|---|
🔍 Missões de Auditoria Interna
| Nº | Tipo | Título / Objeto | Unidade | Período | Fase | Status | % | Ações |
|---|
🌡️ Heatmap (Probabilidade × Impacto)
📊 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 |
|---|
🎛️ 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
Cadastro da planilha — os dois caminhos funcionam. O sistema atualiza automaticamente.
🔍 Registro de Auditoria — últimas 500 ações
| Quando | Usuário | Cargo | Perfil | Ação | Alvo | Detalhe |
|---|
👋 Bem-vindo ao CI Digital
O sistema de controle interno municipal da sua cidade — rápido, acessível e seguro.
🎯 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)
📝 Registrando um Novo Pagamento
👥 Cadastrando um Fornecedor com busca na Receita
⚠️ 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:
- 📝 Criar conta — preencha seus dados e a senha. Sua solicitação fica pendente até o Administrador aprovar.
- 🌱 Primeiro acesso — se o Administrador já te pré-autorizou em lote, use essa aba para criar sua senha e entrar imediatamente.
- 🔑 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):
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.
❓ 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.bre 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.