ssm.ro Docs

Node.js

Sincronizare angajați cu API-ul ssm.ro — Ghid Node.js

Acest ghid arată cum să sincronizezi angajați din platforma HR internă cu API-ul ssm.ro, atât în mod batch (rulare zilnică), cât și în timp real (un singur angajat creat/modificat).

Logica de sincronizare:

  1. Verifică dacă marca este în lista de excluderi → ignoră
  2. Verifică dacă angajatul există în ssm.ro pe baza marca
  3. Dacă există → actualizează
  4. Dacă nu există și statusul nu este rez (reziliat) → creează
  5. Dacă nu există și statusul este rez → ignoră (nu creăm contracte reziliate)

Angajați cu mai multe contracte

Dacă HR-ul emite câte o înregistrare per contract, aceeași persoană apare cu mai multe valori marca și același email. Emailul trebuie să fie unic în organizație, deci sincronizarea trebuie să genereze un alias per contract sau să trimită înregistrarea către o altă organizație — vezi Angajați cu mai multe contracte.

Configurare

const API_BASE = process.env.SSM_API_BASE;     // URL-ul de bază al instalării + versiunea API
const TOKEN = process.env.SSM_API_TOKEN;       // tokenul Bearer primit de la ssm.ro
const ORGANIZATIE = process.env.SSM_ORGANIZATIE; // subdomeniul organizației (ex: "demo-organization")

URL-ul de bază depinde de instalare

Pe o instalare Enterprise (Dedicat), SSM_API_BASE trebuie să conțină domeniul instalării, nu www.appssm.ro — vezi URL de bază.

Setează variabilele de mediu înainte de a rula scriptul:

SSM_API_BASE=https://www.appssm.ro/api/v1 SSM_API_TOKEN=tokenul_tau SSM_ORGANIZATIE=organizatia_ta node sync.js

Lista de excluderi (ignore list)

Unii angajați pot fi excluși din sincronizare pe baza regulilor interne HR — de exemplu administratori tehnici, conturi de test, angajați gestionați manual, sau persoane cu regim special de confidențialitate.

/**
 * Set de mărci excluse explicit din sincronizare.
 *
 * Motivele tipice pentru excludere:
 *   - angajat gestionat manual direct în ssm.ro (fără sursă HR)
 *   - cont tehnic sau de test care nu reprezintă o persoană reală
 *   - persoană cu regim special care nu trebuie sincronizată automat
 *   - angajat cu date HR incorecte, în așteptarea unei corecturi manuale
 *
 * Adaugă sau elimină mărci din acest set ori de câte ori regulile HR se schimbă.
 */
const IGNORED_MARCA = new Set([
  // 'M00001',  // exemplu: cont tehnic
  // 'M00999',  // exemplu: angajat gestionat manual
]);

/**
 * Returnează true dacă marca este în lista de excluderi.
 */
function isIgnored(marca) {
  return IGNORED_MARCA.has(String(marca));
}

Mapare statusuri

Platforma HR poate folosi denumiri interne diferite față de valorile acceptate de API. Funcția de mai jos face conversia.

Valori acceptate de API: activ, suspendat, rez

/**
 * Convertește statusul intern HR → statusul acceptat de API.
 * activ concediu / activ detasat sunt tratate ca suspendat (contract activ, dar temporar indisponibil).
 * reziliat / rez → rez (contract încetat).
 */
function mapStatus(hrStatus) {
  const s = hrStatus?.toLowerCase().trim() ?? '';

  if (s === 'activ') return 'activ';

  if (['activ concediu', 'activ detasat', 'suspendat'].includes(s)) return 'suspendat';

  if (['rez', 'reziliat'].includes(s)) return 'rez';

  // fallback — status necunoscut tratat ca suspendat
  console.warn(`Status necunoscut: "${hrStatus}", tratat ca suspendat`);
  return 'suspendat';
}

Mapare post (grup de risc) din funcția COR

În platformele HR, câmpul post nu este de obicei disponibil — acesta reprezintă grupul de risc al funcției, folosit în ssm.ro pentru a determina categoria de instruire SSM aplicabilă angajatului (ex: lucru la înălțime, risc electric, birou). Fiecare funcție COR (cor) aparține unui grup de risc; majoritatea funcțiilor partajează același grup implicit.

/**
 * Post = grup de risc al funcției, utilizat pentru instruiri SSM.
 * Determină categoria de training aplicată angajatului în platformă
 * (ex: instruire generală, risc electric, lucru la înălțime etc.).
 *
 * Majoritatea funcțiilor COR folosesc postul implicit (DEFAULT_POST).
 * Adaugă excepții doar pentru funcțiile cu un grup de risc distinct.
 */
const DEFAULT_POST = 'Lucratori';

const COR_TO_POST = {
  // 'cor (lowercase)' → 'post / grup de risc'
  //
  // Posturile sunt gestionate în platformă de inspectorii SSM și pot fi create:
  //   - manual, de către inspector, prin interfața platformei
  //   - automat prin API, dacă postul trimis nu există încă — opțiunea de creare automată trebuie activată de super utilizator în setările organizației din platformă
  // Funcțiile COR (cor) sunt alocate posturilor:
  //   - manual de inspector, prin asociere în platformă
  //   - automat prin API, la primul import al funcției — dacă postul nu există, funcția este plasată
  //     temporar sub grupul intern "_FUNCTII_NEALOCATE_" până la alocarea manuală
  // Recomandare: aliniază valorile de post de mai jos cu cele existente în platformă
  // pentru a evita crearea de duplicate sau grupuri neașteptate.
  //
  'medic medicina muncii':            'Medici medicina muncii',
  'inspector protectia muncii':       'Lucratori desemnati SSM',
  'electrician':                      'Lucratori cu risc electric',
  'sudor':                            'Lucratori cu risc chimic si termic',
  'operator macara':                  'Lucratori la inaltime',
  'administrator retele de calcul':   'Lucratori birou',
  // completează cu funcțiile specifice organizației tale
};

/**
 * Returnează postul (grupul de risc SSM) corespunzător funcției COR.
 * Dacă funcția nu are o mapare explicită, se folosește DEFAULT_POST.
 */
function getPost(cor) {
  if (!cor) return DEFAULT_POST;
  return COR_TO_POST[cor.toLowerCase().trim()] ?? DEFAULT_POST;
}

Mapare câmpuri angajat

Adaptează această funcție la structura obiectului din platforma ta HR.

null păstrează valoarea existentă, nu o șterge

În maparea de mai jos, ?? null înseamnă câmpul lipsește din HR, lasă valoarea din platformă neschimbată — API-ul ignoră null și "". Dacă vrei ca o valoare ștearsă în HR să fie anulată și în ssm.ro, trimite un șir nevid în loc de null — de exemplu employee.address ?? '-'. Vezi Contacte.

/**
 * Convertește obiectul HR intern → payload-ul așteptat de API.
 *
 * Exemplu câmpuri HR interne (stânga) → câmpuri API (dreapta):
 *   employee.id          → marca
 *   employee.lastName    → nume
 *   employee.firstName   → prenume
 *   employee.dept        → departament
 *   employee.deptCode    → codDepartament
 *   employee.jobTitle    → cor  (titlul funcției COR din HR)
 *   employee.managerId   → marcaSuperior
 *   employee.manager2Id  → marcaSuperior2
 *   employee.substituteId→ marcaInlocuitor
 *   employee.birthDate   → dataNasterii (text liber: dată sau „dată, loc”)
 *   employee.status      → status (trecut prin mapStatus)
 *   employee.phone       → telefon
 *   employee.cnp         → cnp (13 cifre, unic în organizație)
 *   employee.language    → limba (ro, en)
 *   employee.qualification → calificare
 *   employee.citizenship → cetatenie
 *   employee.nationality → nationalitate
 *   employee.startDate   → dataIncepereActivitate (format: YYYY-MM-DD)
 *
 * post (grup de risc SSM) este derivat din cor prin getPost(),
 * deoarece HR-ul nu îl expune de obicei ca un câmp separat.
 */
function mapEmployee(employee) {
  return {
    organizatie:      ORGANIZATIE,
    marca:            String(employee.id),
    nume:             employee.lastName,
    prenume:          employee.firstName,
    email:            employee.email ?? null,
    // departament este creat automat în platformă dacă nu există deja.
    // Notă: crearea automată a departamentelor trebuie activată de super utilizator în setările organizației din platformă.
    // Dacă organizația are departamente cu denumiri identice (ex: "Producție" în mai multe locații),
    // trimite codDepartament (codul extern unic din HR) pentru a identifica exact departamentul corect
    // și a evita alocarea greșită a angajatului.
    // Departamentul este identificat după codDepartament: dacă trimiți același codDepartament
    // cu o denumire (departament) diferită, denumirea existentă este ACTUALIZATĂ (redenumire),
    // fără a crea un departament nou — o redenumire în HR se propagă automat în platformă.
    departament:      employee.dept ?? null,
    codDepartament:   employee.deptCode ?? null,
    cor:              employee.jobTitle ?? null,  // titlul funcției COR, nu codul numeric
    // post = grup de risc SSM, derivat din funcția COR; nu vine din HR
    post:             getPost(employee.jobTitle),
    marcaSuperior:    employee.managerId    ? String(employee.managerId)    : null,
    marcaSuperior2:   employee.manager2Id   ? String(employee.manager2Id)   : null,
    marcaInlocuitor:  employee.substituteId ? String(employee.substituteId) : null,
    adresa:           employee.address ?? null,
    localitate:       employee.city ?? null,
    judet:            employee.county ?? null,
    dataNasterii:     employee.birthDate ?? null,  // text liber: dată sau „dată, loc”
    locatieFizica:    employee.officeLocation ?? null,
    status:           mapStatus(employee.status),
    echipaPSI:        employee.psiTeam ? 'Da' : 'Nu',
    telefon:          employee.phone ?? null,
    cnp:              employee.cnp ?? null,             // 13 cifre, unic în organizație
    limba:            employee.language ?? null,        // valori acceptate: ro, en
    calificare:       employee.qualification ?? null,
    cetatenie:        employee.citizenship ?? null,
    nationalitate:    employee.nationality ?? null,
    dataIncepereActivitate: employee.startDate ?? null, // format: YYYY-MM-DD
  };
}

Funcții API

Modificare API — septembrie 2026

Căutarea contactului se face acum prin GET /contacts?organizatie=&marca=. Ruta veche GET /contacts/{marca} este depreciată — vezi Contacte.

const headers = () => ({
  'Authorization': `Bearer ${TOKEN}`,
  'Content-Type': 'application/json',
});

/**
 * Returnează datele contactului sau null dacă nu există.
 * Marca se trimite ca parametru de query, codificată cu encodeURIComponent() —
 * ruta veche /contacts/{marca} este depreciată.
 */
async function getContact(marca) {
  const res = await fetch(
    `${API_BASE}/contacts?organizatie=${ORGANIZATIE}&marca=${encodeURIComponent(marca)}`,
    { headers: headers() }
  );
  if (res.status === 404) return null;
  if (!res.ok) throw new Error(`GET /contacts?marca=${marca} → ${res.status}`);
  return res.json();
}

/**
 * Creează un contact nou. Aruncă eroare dacă marca există deja.
 */
async function createContact(payload) {
  const res = await fetch(`${API_BASE}/contacts?organizatie=${ORGANIZATIE}`, {
    method: 'POST',
    headers: headers(),
    body: JSON.stringify(payload),
  });
  if (!res.ok) {
    const body = await res.json().catch(() => ({}));
    throw new Error(`POST /contacts → ${res.status}: ${body.error ?? 'eroare necunoscuta'}`);
  }
  return res.json();
}

/**
 * Actualizează un contact existent identificat prin marca.
 * Returnează null dacă nu au fost detectate diferențe (204 No Content).
 */
async function updateContact(payload) {
  const res = await fetch(`${API_BASE}/contacts/update?organizatie=${ORGANIZATIE}`, {
    method: 'PATCH',
    headers: headers(),
    body: JSON.stringify(payload),
  });
  if (res.status === 204) return null;
  if (!res.ok) {
    const body = await res.json().catch(() => ({}));
    throw new Error(`PATCH /contacts/update → ${res.status}: ${body.error ?? 'eroare necunoscuta'}`);
  }
  return res.json();
}

Sincronizare un singur angajat

Folosit pentru evenimente în timp real (angajat creat sau modificat în HR).

async function syncEmployee(hrEmployee) {
  const payload = mapEmployee(hrEmployee);
  const marca = payload.marca;

  // Angajatul este exclus explicit din sincronizare prin reguli HR interne.
  if (isIgnored(marca)) {
    console.log(`[${marca}] Ignorat — exclus din sincronizare (IGNORED_MARCA)`);
    return { marca, action: 'skipped', reason: 'ignored' };
  }

  const existing = await getContact(marca);

  if (existing) {
    const result = await updateContact(payload);
    if (result === null) {
      console.log(`[${marca}] Fara modificari (204)`);
      return { marca, action: 'no_change' };
    }
    console.log(`[${marca}] Actualizat`);
    return { marca, action: 'updated' };
  } else {
    if (payload.status === 'rez') {
      console.log(`[${marca}] Ignorat — contact reziliat, nu se creeaza`);
      return { marca, action: 'skipped', reason: 'reziliat' };
    }
    await createContact(payload);
    console.log(`[${marca}] Creat`);
    return { marca, action: 'created' };
  }
}

Sincronizare batch zilnică

Primește un array de angajați nesincronizați și îi procesează secvențial.

async function syncAll(hrEmployees) {
  const summary = { created: 0, updated: 0, skipped: 0, no_change: 0, errors: 0 };

  for (const employee of hrEmployees) {
    try {
      const result = await syncEmployee(employee);
      summary[result.action] = (summary[result.action] ?? 0) + 1;
    } catch (err) {
      const marca = employee.id ?? '?';
      console.error(`[${marca}] Eroare: ${err.message}`);
      summary.errors++;
    }
  }

  console.log('\n--- Sumar sincronizare ---');
  console.log(`Creati:        ${summary.created}`);
  console.log(`Actualizati:   ${summary.updated}`);
  console.log(`Fara modif.:   ${summary.no_change}`);
  console.log(`Ignorati:      ${summary.skipped}`);
  console.log(`Erori:         ${summary.errors}`);

  // Trimite/loghează raportul de execuție către client.
  // Întotdeauna dacă există erori; opțional pentru rulări fără probleme.
  //
  // Exemple de implementare:
  //   - email către echipa HR: sendReportEmail(summary, errorDetails)
  //   - POST către un webhook intern: await fetch(REPORT_WEBHOOK_URL, { method: 'POST', body: JSON.stringify(summary) })
  //   - scriere în fișier de log: appendFileSync('./sync.log', JSON.stringify({ date: new Date(), ...summary }) + '\n')
  //   - trimitere în Slack / Teams: await notifyChannel(summary)
  //
  // if (summary.errors > 0) {
  //   await sendReportEmail(summary);  // întotdeauna la erori
  // } else {
  //   //   await sendReportEmail(summary);  // decomentează dacă vrei raport și la succes
  // }

  return summary;
}

Exemple de utilizare

Batch zilnic (fișier JSON)

// sync.js
import { readFileSync } from 'fs';

// employees.json — lista exportată din HR pentru ziua curentă
const employees = JSON.parse(readFileSync('./employees.json', 'utf8'));

syncAll(employees).then((summary) => {
  if (summary.errors > 0) process.exit(1);
});

Rulare:

SSM_API_TOKEN=abc123 SSM_ORGANIZATIE=demo-organization node sync.js

Eveniment în timp real (un singur angajat)

// Apelat dintr-un webhook, queue consumer sau orice sistem de evenimente HR
async function onEmployeeChanged(hrEmployee) {
  try {
    const result = await syncEmployee(hrEmployee);
    console.log('Sync result:', result);
  } catch (err) {
    console.error('Sync failed:', err.message);
    // re-aruncă pentru a permite retry-ul din sistemul de mesagerie
    throw err;
  }
}

// Exemplu de apel direct
onEmployeeChanged({
  id: 'M00212',
  lastName: 'Popescu',
  firstName: 'Ion',
  email: 'ion.popescu@firma.ro',
  dept: 'Resurse Umane',
  deptCode: 'RU-01',
  jobTitle: 'Specialist resurse umane',
  managerId: 'M00100',
  birthDate: '1985-06-15',
  status: 'activ',
  psiTeam: false,
  phone: '+40722333444',
  cnp: '1850615221144',
  language: 'ro',
  qualification: 'Economist',
  citizenship: 'Romana',
  nationality: 'Romania',
  startDate: '2020-01-15',
});

Coduri de răspuns API

CodSemnificație
200Succes — returnează datele contactului
204Succes — nicio modificare detectată (doar la PATCH)
400Organizatie invalidă sau lipsă
401Token lipsă sau invalid
404Contactul nu a fost găsit (doar la GET)
422Date invalide — răspunsul include { "error": "..." } cu detalii
500Eroare internă server