ssm.ro Docs
API pentru DezvoltatoriExemple implementare

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)

Configurare

const API_BASE = 'https://www.appssm.ro/api/v1';
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")

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

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.

/**
 * 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 (format: YYYY-MM-DD)
 *   employee.status      → status (trecut prin mapStatus)
 *
 * 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.
    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,  // format: YYYY-MM-DD
    locatieFizica:    employee.officeLocation ?? null,
    status:           mapStatus(employee.status),
    echipaPSI:        employee.psiTeam ? 'Da' : 'Nu',
  };
}

Funcții API

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

/**
 * Returnează datele contactului sau null dacă nu există.
 */
async function getContact(marca) {
  const res = await fetch(
    `${API_BASE}/contacts/${encodeURIComponent(marca)}?organizatie=${ORGANIZATIE}`,
    { headers: headers() }
  );
  if (res.status === 404) return null;
  if (!res.ok) throw new Error(`GET /contacts/${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,
});

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