Entre com o e-mail e a senha cadastrados em Acessos_Painel.
Painel executivo de acompanhamento das metas corporativas, faróis de prazo, responsáveis e evolução de entrega.
Fonte: carregando dados do Google Sheets...
Área exclusiva do admin. Os dados são gravados na aba Usuarios do Google Sheets.
Importe a base XLSX no Google Drive, cole o Apps Script abaixo e publique como App da Web. Depois cole a URL aqui.
/**
* Apps Script v5 - Painel Metas IGA 2026 com login, permissões e configurações
*
* Abas:
* 1) Metas_Editaveis
* 2) Acessos_Painel
*
* Aba Acessos_Painel:
* Email | Nome | Perfil | Responsável | Senha | Ativo
*
* Perfis:
* admin = visualiza tudo, edita metas e gerencia usuários
* diretoria = visualiza tudo, não edita
* responsavel = visualiza metas próprias, metas em que aparece em Envolvidos + globais, não edita
*/
const SHEET_NAME = 'Metas_Editaveis';
const ACCESS_SHEET_NAME = 'Acessos_Painel';
function getSpreadsheet_() {
return SpreadsheetApp.getActiveSpreadsheet();
}
function normalize_(value) {
return String(value || '')
.toLowerCase()
.normalize('NFD')
.replace(/[\\u0300-\\u036f]/g, '')
.trim();
}
function ensureAccessSheet_() {
const ss = getSpreadsheet_();
let sh = ss.getSheetByName(ACCESS_SHEET_NAME);
if (!sh) {
sh = ss.insertSheet(ACCESS_SHEET_NAME);
sh.getRange(1, 1, 1, 6).setValues([[
'Email',
'Nome',
'Perfil',
'Responsável',
'Senha',
'Ativo'
]]);
sh.getRange(2, 1, 1, 6).setValues([[
'admin@iga-go.com.br',
'Administrador',
'admin',
'',
'admin123',
'SIM'
]]);
}
return sh;
}
function getAccessList_() {
const sh = ensureAccessSheet_();
const values = sh.getDataRange().getValues();
if (!values.length) return [];
const headers = values[0].map(h => String(h || '').trim());
return values.slice(1)
.filter(row => row.some(cell => String(cell || '').trim() !== ''))
.map((row, idx) => {
const obj = {};
headers.forEach((h, i) => obj[h] = row[i]);
return {
rowNumber: idx + 2,
email: String(obj.Email || '').trim().toLowerCase(),
nome: String(obj.Nome || '').trim(),
perfil: normalize_(obj.Perfil || 'responsavel'),
responsavel: String(obj['Responsável'] || obj.Responsavel || '').trim(),
senha: String(obj.Senha || '').trim(),
ativo: normalize_(obj.Ativo || 'SIM') !== 'nao'
};
});
}
function validateLogin_(email, senha) {
email = String(email || '').trim().toLowerCase();
senha = String(senha || '').trim();
const access = getAccessList_().find(u =>
u.email === email &&
u.senha === senha &&
u.ativo
);
if (!access) {
throw new Error('Login inválido ou usuário inativo.');
}
return access;
}
function isAdmin_(access) {
return normalize_(access && access.perfil) === 'admin';
}
function splitResponsaveis_(value) {
return String(value || '')
.split(/[,;|\/]+|\s+e\s+/i)
.map(normalize_)
.filter(Boolean);
}
function responsavelMatches_(metaResp, userResp) {
const metas = splitResponsaveis_(metaResp);
const users = splitResponsaveis_(userResp);
if (!metas.length || !users.length) return false;
return metas.some(m => users.some(u =>
m === u ||
(u.length >= 4 && m.indexOf(u) !== -1) ||
(m.length >= 4 && u.indexOf(m) !== -1)
));
}
function canSeeRow_(row, access) {
const perfil = normalize_(access && access.perfil);
if (perfil === 'admin' || perfil === 'diretoria') return true;
const tipo = normalize_(row.Tipo);
const resp = row['Responsável'] || row.Responsavel || '';
const respNorm = normalize_(resp);
if (tipo === 'global' || respNorm === 'global' || respNorm === 'todos') return true;
if (!access) return false;
const userResp = access.responsavel || access.nome || String(access.email || '').split('@')[0];
return responsavelMatches_(resp, userResp);
}
function sheetToObjects_() {
const ss = getSpreadsheet_();
const sh = ss.getSheetByName(SHEET_NAME);
if (!sh) throw new Error('Aba não encontrada: ' + SHEET_NAME);
const values = sh.getDataRange().getValues();
if (!values.length) return [];
const headers = values[0].map(h => String(h || '').trim());
return values.slice(1)
.filter(row => row.some(cell => String(cell || '').trim() !== ''))
.map(row => {
const obj = {};
headers.forEach((h, i) => {
let value = row[i];
if (value instanceof Date) {
value = Utilities.formatDate(value, Session.getScriptTimeZone(), 'dd/MM/yyyy');
}
obj[h] = value === null || value === undefined ? '' : String(value);
});
return obj;
});
}
function output_(payload, callback) {
const json = JSON.stringify(payload);
if (callback) {
return ContentService
.createTextOutput(callback + '(' + json + ');')
.setMimeType(ContentService.MimeType.JAVASCRIPT);
}
return ContentService
.createTextOutput(json)
.setMimeType(ContentService.MimeType.JSON);
}
function publicAccessList_(access) {
if (!isAdmin_(access)) return [];
return getAccessList_().map(u => ({
email: u.email,
nome: u.nome,
perfil: u.perfil,
responsavel: u.responsavel,
ativo: u.ativo
}));
}
function getBootstrapPayload_(email, senha) {
const access = validateLogin_(email, senha);
const allRows = sheetToObjects_();
const visibleRows = allRows.filter(row => canSeeRow_(row, access));
return {
ok: true,
count: visibleRows.length,
rows: visibleRows,
access: {
email: access.email,
nome: access.nome,
perfil: access.perfil,
responsavel: access.responsavel,
canEdit: isAdmin_(access),
canConfig: isAdmin_(access),
ativo: access.ativo
},
accessList: publicAccessList_(access)
};
}
function upsertUser_(payload, adminAccess) {
if (!isAdmin_(adminAccess)) {
throw new Error('Acesso negado. Apenas admin pode gerenciar usuários.');
}
const email = String(payload.email || payload.Email || '').trim().toLowerCase();
const nome = String(payload.nome || payload.Nome || '').trim();
const perfil = normalize_(payload.perfil || payload.Perfil || 'responsavel');
const responsavel = String(payload.responsavel || payload['Responsável'] || '').trim();
const senha = String(payload.senha || payload.Senha || '').trim();
const ativo = normalize_(payload.ativo || payload.Ativo || 'SIM') === 'sim' ? 'SIM' : 'NÃO';
if (!email) throw new Error('E-mail obrigatório.');
if (!nome) throw new Error('Nome obrigatório.');
if (!['admin','diretoria','responsavel'].includes(perfil)) throw new Error('Perfil inválido.');
if (!senha) throw new Error('Senha obrigatória.');
const sh = ensureAccessSheet_();
const users = getAccessList_();
const existing = users.find(u => u.email === email);
const rowValues = [email, nome, perfil, responsavel, senha, ativo];
if (existing) {
sh.getRange(existing.rowNumber, 1, 1, 6).setValues([rowValues]);
} else {
sh.appendRow(rowValues);
}
return { ok: true, email: email };
}
function doGet(e) {
try {
const callback = e && e.parameter ? e.parameter.callback : null;
const action = e && e.parameter ? String(e.parameter.action || 'bootstrap') : 'bootstrap';
const email = e && e.parameter ? e.parameter.email : '';
const senha = e && e.parameter ? e.parameter.senha : '';
if (action === 'bootstrap' || action === 'login') {
return output_(getBootstrapPayload_(email, senha), callback);
}
return output_({ ok: false, error: 'Ação inválida.' }, callback);
} catch (err) {
return output_({
ok: false,
error: String(err && err.message ? err.message : err)
}, e && e.parameter ? e.parameter.callback : null);
}
}
function doPost(e) {
try {
const payload = JSON.parse((e && e.postData && e.postData.contents) || '{}');
const action = String(payload.__action || 'saveMeta');
const access = validateLogin_(payload.__email, payload.__senha);
if (action === 'saveUser') {
const result = upsertUser_(payload, access);
return output_({
ok: true,
result: result,
accessList: publicAccessList_(access)
}, null);
}
if (!isAdmin_(access)) {
throw new Error('Acesso negado. Apenas perfil admin pode editar metas.');
}
const ss = getSpreadsheet_();
const sh = ss.getSheetByName(SHEET_NAME);
if (!sh) throw new Error('Aba não encontrada: ' + SHEET_NAME);
const id = String(payload.ID || '').trim();
if (!id) throw new Error('ID vazio');
const values = sh.getDataRange().getValues();
const headers = values[0].map(h => String(h || '').trim());
const idCol = headers.indexOf('ID');
if (idCol < 0) throw new Error('Coluna ID não encontrada');
let targetRow = -1;
for (let i = 1; i < values.length; i++) {
if (String(values[i][idCol]).trim() === id) {
targetRow = i + 1;
break;
}
}
if (targetRow < 0) throw new Error('ID não encontrado: ' + id);
const editableFields = [
'Tipo',
'Responsável',
'Envolvidos',
'Meta',
'Indicador',
'Valor Meta',
'Unidade',
'Farol 80%',
'Farol 100%',
'Farol 120%',
'Peso %',
'Prazo',
'Status',
'Cumprimento %',
'Realizado',
'Evidência/Link',
'Observações',
'Relatório Qualitativo'
];
editableFields.forEach(name => {
const col = headers.indexOf(name);
if (col >= 0 && Object.prototype.hasOwnProperty.call(payload, name)) {
sh.getRange(targetRow, col + 1).setValue(payload[name]);
}
});
const obsCol = headers.indexOf('Observações');
if (obsCol >= 0) {
sh.getRange(targetRow, obsCol + 1)
.setNote('Atualizado pelo painel HTML em ' + new Date() + ' por ' + access.email);
}
return output_({
ok: true,
id: id,
updatedBy: access.email
}, null);
} catch (err) {
return output_({
ok: false,
error: String(err && err.message ? err.message : err)
}, null);
}
}
Informações carregadas da aba Acessos_Painel.