ECHO HRH
Verifying session…
🔥
ECHO HRH SYSTEM
ECHO OpsHub
NATIONAL
AD
Staff / OCBs (Activos)
0
0 províncias
Activos
0
— % do total
Staff Técnico
0
0 actores comunit.
Partner OCBs
0
0 grants activos
Subcontracted
0
0 activos
Parceiros
0
Massa Salarial (MZN)
0
mensal bruto
Contratos Expirando
0
próximos 30 dias
Expiram 31–60d
0
próximo mês
Índice Paridade
F/M (meta ≥ 0.85)
Rotatividade
—%
taxa de saída
Com Nível Superior
0
— %
Salário Médio
0
MZN mensal

Distribuição por Província

Staff por Parceiro × Tipo

🏘️ OCBs por Província

ProvínciaOCBsGrants ActivosTotal Financiado

⚠️ Contratos a Expirar (90 dias)

NomeProvínciaFimDias

Staff por Função (Top 10)

🏘️ Por OCB / Parceiro

Parceiro Staff Total Activos Massa Sal. MZN Sal. Médio

🗺 Por Província (Staff)

Província Total Activos Massa Sal. MZN

Lista de Trabalhadores

NomeParceiroProvínciaFunção TipoStatusFim ContratoSalário
Filtrar por Parceiro
Nome ↕ Parceiro ↕ Província ↕ Função ↕ Cadre HRH StatusFinanciador Início ↕ Contract Salário ↕ Acções
OCBLocalização TipoStaffMassa Sal. MZNGrantsExecuçãoSaldo/TotalContactoAcções
# Nome Organização Posição / Colocação Província Início Fim Sal. Base CBE Fee 9% Sal. Líquido Status Acções
Total Grants
0
Award Ceiling
$0
USD total
Disbursed
$0
Balance
$0
undisbursed
Expiring 60d
0
urgent
Loading grants…
Total Disbursements
0
Total Value
$0
USD received
Latest Disbursement
Avg per Disbursement
$0
DateOCBGrantTrancheAmountCurrencyPayment Ref.Notes
Total Modifications
0
Total Ceiling Added
$0
across all mods
Total Obligated Added
$0
net increases
Overspending Alerts
0
grants exceed obligated
Loading modifications…
Para onde pretende importar?
👥
Staff / Workers
Trabalhadores das OCBs parceiras. Colunas: Nome, Parceiro, NUIT, Posição, Salário, Distrito…
🏘️
Partner OCBs
Community-Based Organisations. Colunas: Nome OCB, Tipo, Província, Distrito, NIF, Contacto…
🤝
Subcontracted
Staff CBE, Nweti e outros subcontratados. Colunas: Nome, Posição, Colocação, Sal. Base, CBE Fee 9%, INSS, IRPS…
Data/HoraUtilizadorAcção ColecçãoDocumentoProvínciaAlterações
Carregue os logs de auditoria

Adicionar Utilizador Firebase

As permissões são definidas automaticamente pelo perfil seleccionado.

Utilizadores Registados

NomeEmailPerfilÂmbitoActivoÚltimo AcessoAcções

Adicionar Província

Províncias e Distritos

Adicionar Cadre HRH

CadreInteraction TypeAcções
Email Notification Settings
Configure recipients and thresholds per module. Alerts processed by Cloud Function via Resend.
Checking last send…
⏰ Alert thresholds (applies to all modules)
📋
Grants
Expiring grants, low disbursement
👥
HR
Staff contracts, subcontracted expiry
📝
Procurement
Rents, internet, security, suppliers
⚙️
System-wide
Fallback — receives all alerts
These addresses receive all alert types regardless of module settings above.

🔐 Firestore Security Rules — complete ruleset

Copy and paste the entire block below into Firebase Console → Firestore → Rules. Replace everything.

⚠️ Key fix in v2: allow read: if isAuth(); on the users collection — required so that getRole() can resolve correctly when evaluating write permissions on grants, OCBs, etc. Without this, all writes fail with permission-denied even for admins.
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    function isAuth() { return request.auth != null; }

    // getRole() reads the user's own profile — works correctly because
    // users allow read: if isAuth() (any authenticated user can read profiles)
    function getRole() {
      return isAuth()
        ? get(/databases/$(database)/documents/users/$(request.auth.uid)).data.get('role', '')
        : '';
    }
    function canWrite() {
      return isAuth() && getRole() in ['admin','gestor_nacional','gestor_provincial'];
    }
    function canWriteSubcontr() {
      return isAuth() && getRole() in ['admin','gestor_nacional','gestor_provincial','gestor_subcontr'];
    }
    function isOwner() {
      return isAuth() && request.auth.uid == resource.data.authorId;
    }

    // ── Users ──
    // allow read: if isAuth() is intentional — getRole() needs to read any user profile
    // to evaluate write permissions across all other collections.
    match /users/{uid} {
      allow read:   if isAuth();
      allow create: if isAuth();
      allow update: if isAuth() && (request.auth.uid == uid || getRole() == 'admin');
      allow delete: if isAuth() && getRole() == 'admin';
      allow list:   if isAuth() && getRole() in ['admin','gestor_nacional'];
    }

    // ── Workers / Staff ──
    match /workers/{wid} {
      allow read:           if isAuth();
      allow create, update: if canWrite();
      allow delete:         if isAuth() && getRole() in ['admin','gestor_nacional'];
      match /history/{hid}  { allow read: if isAuth(); allow write: if canWrite(); }
      match /comments/{cid} {
        allow read, create: if isAuth();
        allow update:       if isAuth() && (isOwner() || getRole() in ['admin','gestor_nacional']);
        allow delete:       if isAuth() && (isOwner() || getRole() in ['admin','gestor_nacional']);
      }
    }

    // ── OCBs and all grant sub-collections ──
    match /ocbs/{oid} {
      allow read:           if isAuth();
      allow create, update: if canWrite();
      allow delete:         if isAuth() && getRole() in ['admin','gestor_nacional'];
      match /comments/{cid} {
        allow read, create: if isAuth();
        allow update, delete: if isAuth() && (isOwner() || getRole() in ['admin','gestor_nacional']);
      }
      match /grants/{gid} {
        allow read:  if isAuth();
        allow write: if canWrite();
        match /desembolsos/{did}  { allow read: if isAuth(); allow write: if canWrite(); }
        match /historico/{hid}    { allow read: if isAuth(); allow write: if canWrite(); }
        match /modificacoes/{mid} { allow read: if isAuth(); allow write: if canWrite(); }
        match /inkind_items/{iid} { allow read: if isAuth(); allow write: if canWrite(); }
        match /despesas/{did2}    { allow read: if isAuth(); allow write: if canWrite(); }
        match /comments/{cid} {
          allow read, create: if isAuth();
          allow update, delete: if isAuth() && (isOwner() || getRole() in ['admin','gestor_nacional']);
        }
      }
    }

    // ── Subcontracted ──
    match /subcontratados/{doc} {
      allow read:           if isAuth();
      allow create, update: if canWriteSubcontr();
      allow delete:         if isAuth() && getRole() in ['admin','gestor_nacional'];
      match /notas/{nid} {
        allow read:    if isAuth();
        allow create:  if canWriteSubcontr();
        allow delete:  if isAuth() && (isOwner() || getRole() in ['admin','gestor_nacional']);
        allow update:  if false;
      }
      match /historico/{hid} { allow read: if isAuth(); allow write: if canWriteSubcontr(); }
      match /comments/{cid} {
        allow read, create: if isAuth();
        allow update, delete: if isAuth() && (isOwner() || getRole() in ['admin','gestor_nacional']);
      }
    }

    // ── Team feed ──
    match /team_feed/{doc} {
      allow read, create: if isAuth();
    }

    // ── App config ──
    match /config/{doc} {
      allow read:  if isAuth();
      allow write: if isAuth() && getRole() == 'admin';
    }

    // ── Audit log ──
    match /audit_logs/{doc} {
      allow read:           if isAuth() && getRole() in ['admin','gestor_nacional'];
      allow create:         if isAuth();
      allow update, delete: if false;
    }

    // ── Notification queue (Cloud Function processes these) ──
    match /notification_queue/{doc} {
      allow read:           if isAuth() && getRole() in ['admin','gestor_nacional'];
      allow create:         if isAuth();
      allow update, delete: if false;
    }
    match /user_notifications/{uid}/items/{id} {
      allow read, write: if isAuth() && request.auth.uid == uid;
    }

    // ── Email queue (fila_email — Cloud Function trigger) ──
    match /fila_email/{doc} {
      allow read:           if isAuth();
      allow create:         if isAuth();
      allow update, delete: if false;
    }

    // ── Presence ──
    match /presence/{uid} {
      allow read:  if isAuth();
      allow write: if isAuth() && request.auth.uid == uid;
    }

    // ── Chat ──
    match /chat_messages/{channel}/messages/{mid} {
      allow read, create: if isAuth();
      allow delete: if isAuth() && (isOwner() || getRole() in ['admin','gestor_nacional']);
      allow update: if false;
    }
    match /chat_messages/{channel}/typing/{uid} {
      allow read:  if isAuth();
      allow write: if isAuth() && request.auth.uid == uid;
    }

    // ── Procurement contracts ──
    match /contratos_procurement/{cid} {
      allow read:  if isAuth();
      allow write: if isAuth() && getRole() in ['admin','gestor_nacional','procurement','operacoes'];
      match /adendas/{aid}    { allow read: if isAuth(); allow write: if isAuth() && getRole() in ['admin','gestor_nacional','procurement','operacoes']; }
      match /historico/{hid}  { allow read: if isAuth(); allow write: if isAuth() && getRole() in ['admin','gestor_nacional','procurement','operacoes']; }
    }

    // ── Deny everything else ──
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

📋 PEPFAR HRH Inventory Export

Gera o ficheiro Excel no formato PEPFAR HRH Inventory (FY25) pronto para submissão no DATIM. Inclui 4 sheets: Cover, HRH_Inventory, Summary_by_Cadre, Summary_by_Province.

Colunas geradas: record_number, fiscal_year, operating_unit, prime_partner_name, interaction_type, cadre, employment_title, mode_of_hiring, is_community_worker, work_location_name, work_location_province, sex, fte, base_salary_usd, fringe_benefits_usd, total_compensation_usd e mais.

Qualidade dos Dados

Clique em "Analisar" para verificar a qualidade dos dados.

🤝 Subcontracted — Corrigir Organização

Registos importados sem o campo organizacao preenchido não aparecem para o Gestor de Subcontratos. Esta ferramenta estampa a organização em todos os registos que não têm este campo.

🧹 Limpeza de Campos

Normalizar Sexo
Converte "M"→"Masculino" e "F"→"Feminino" em todos os registos de staff e subcontratados
Normalizar Nomes
Corrige capitalização: "JOÃO SILVA" → "João Silva"
Limpar Espaços Extra
Remove espaços duplos e leading/trailing em todos os campos de texto
Normalizar Números de Contacto
Remove espaços e caracteres inválidos dos campos NUIT, INSS e Contacto
Preencher Status em Branco
Registos sem campo status recebem "Activo" por defeito
Recalcular Totais
Recalcula totalBruto (salário+subsídio+outros) e totalPepfarUSD para todos os registos

🔎 Duplicados & Validação

Encontrar Duplicados por NUIT
Lista workers com o mesmo NUIT (possíveis registos duplicados)
Encontrar Duplicados por Nome
Lista nomes idênticos (pode ter variações de ortografia)
Registos Incompletos
Lista registos sem nome, sem salário ou sem província
Datas Inválidas
Encontra registos com dataFim anterior a dataInicio
Exportar Relatório de Qualidade
CSV com todos os problemas encontrados e IDs dos documentos

📊 Score de Completude por Parceiro / OCB

Clique em "Calcular" para ver o ranking de qualidade de dados por organização.

⚠️ Detecção de Anomalias Salariais

Detecta salários inesperadamente altos ou baixos para a mesma posição e província. Útil para encontrar erros de digitação e casos reais de disparidade.

🗓️ Revisão Periódica de Dados

Rastreio de quando cada parceiro/OCB confirmou pela última vez que os seus dados estão actualizados. Recomendado a cada 90 dias.

Clique em "Ver Estado" para verificar as revisões pendentes.

🇺🇸 Conformidade PEPFAR/DATIM

Verifica se cada trabalhador tem os campos obrigatórios para submissão PEPFAR: Cadre HRH, Interaction Type, Ano Fiscal, Financiador e valores USD preenchidos.

📥 Grants Staging — Ligar Registos a Grants

Registos do Excel sem grant criado ficam aqui em staging. Clique ⚡ Auto-ligar Todos depois de criar os grants — os registos com Grant No. identificado são ligados automaticamente.

Clique em "Carregar" para ver os registos pendentes.

⚠️ Zona de Perigo

Acções irreversíveis. Faça export antes de prosseguir.

⛔ Acção nuclear — apaga TUDO sem excepção