# @zecimo/websdk

JavaScript SDK pentru integrarea widget-urilor zecimo în site-uri externe (iframe-based).

Expune widget-uri embed peste aceeași logică de backend ca aplicația principală (pontajele scriu prin `HrTimesheetService`, deci rezultatul în DB este identic):

- **`timesheet`** — pontaj, varianta pentru **contabil**: grilă pe taburi, selector de lună `<`/`>`.
- **`employerTimesheet`** — **Pontaj Angajator**: wizard ghidat în 7 pași pentru administratorul firmei (listă de pontaje → completare pas-cu-pas pentru toți angajații → trimitere → rezumat read-only).
- **`employees`** — **Angajați**: lista angajaților firmei (tabel + căutare + buton _Angajat nou_); click pe un rând → fișa completă a angajatului (aceeași ca la contabil), fără REGES, fără documente contabile și fără editări — doar încărcare de documente și adăugare de rețineri.

Restul vor urma (`payroll`, etc.).

## Instalare

Via script tag (recomandat pentru integrări simple):

```html
<script src="https://embed.zecimo.ro/web-sdk.js"></script>
```

Sau via npm:

```bash
pnpm add @zecimo/websdk
```

```ts
import { zecimoSdk } from '@zecimo/websdk';
```

## Flow-ul de integrare

```
[backend client]  ──POST /developer/embed/session──> { sessionToken }
                   (header: X-API-Key)

[frontend client] ──new zecimoSdk().timesheet({ sessionToken, elementId })─────────> iframe (contabil)
                  ──new zecimoSdk().employerTimesheet({ sessionToken, elementId })──> iframe (angajator)
                  ──new zecimoSdk().employees({ sessionToken, elementId })─────────> iframe (angajați)
```

Același `sessionToken` funcționează pentru toate widget-urile.

### 1. Obține o cheie API (o dată)

Un admin de workspace creează cheia din **Admin → Developer → API Keys**. Permisiune necesară: `developer.api-keys.manage`.

### 2. Backend: schimbă cheia API contra unui session token

Session tokenul este un JWT legat de `(workspaceId, companyId)`, valabil **12h** (configurabil pe server prin `EMBED_SESSION_EXPIRY_HOURS`). Luna se alege din UI.

```bash
curl -X POST https://api.zecimo.ro/developer/embed/session \
  -H 'X-API-Key: prod_…' \
  -H 'Content-Type: application/json' \
  -d '{ "cui": "RO12345678" }'
```

Răspuns:

```json
{
    "data": {
        "sessionToken": "eyJhbGc…",
        "expiresAt": "2026-05-06T12:34:56.000Z"
    }
}
```

Erori posibile: `401` (cheie API invalidă), `404` (CUI inexistent în workspace).

### 3. Frontend: montează widget-ul

```html
<div id="zecimo-timesheet"></div>

<script src="https://embed.zecimo.ro/web-sdk.js"></script>
<script>
    const sdk = new zecimoSdk();

    const widget = sdk.timesheet({
        sessionToken: '<din pasul 2>',
        elementId: 'zecimo-timesheet',
        onEvent: e => console.log(e.type, e.payload),
    });

    // mai târziu:
    // widget.unmount();
</script>
```

Pentru varianta angajator sau lista de angajați, schimbă doar metoda (`employerTimesheet` / `employees`) — opțiunile sunt identice:

```js
const widget = sdk.employerTimesheet({
    sessionToken: '<din pasul 2>',
    elementId: 'zecimo-timesheet',
    onEvent: e => console.log(e.type, e.payload),
});

// lista de angajați (read-only)
const employees = sdk.employees({
    sessionToken: '<din pasul 2>',
    elementId: 'zecimo-employees',
    onEvent: e => console.log(e.type, e.payload),
});
```

### Înălțime

Implicit, widget-ul **se redimensionează automat** după conținut. Dacă vrei o înălțime fixă (widget-ul umple spațiul și derulează intern), pasează `height`:

```js
// umple containerul (care trebuie să aibă o înălțime definită)
sdk.employerTimesheet({ sessionToken, elementId: 'zecimo-timesheet', height: '100%' });

// înălțime fixă în px
sdk.employerTimesheet({ sessionToken, elementId: 'zecimo-timesheet', height: 600 });
```

### Spațiere

Widget-urile se randează **fără padding exterior** — conținutul începe de la marginea iframe-ului. Spațierea o decide pagina gazdă, ca widgetul să se așeze la fel ca restul interfeței tale: pune padding pe containerul (`elementId`) în care montezi, sau injectează-l prin `customCss`.

```css
/* în pagina gazdă */
#zecimo-employees {
    padding: 1.5rem;
}
```

### Temă

Widget-ul se randează implicit pe temă deschisă. Când pagina gazdă e întunecată, pasează `theme: 'dark'` — paleta se aplică înainte de prima randare, deci nu clipește alb la încărcare.

```js
sdk.employees({
    sessionToken,
    elementId: 'zecimo-employees',
    theme: 'dark',
});
```

Fundalul iframe-ului rămâne transparent în ambele teme, deci widgetul se așază pe fundalul paginii tale.

### CSS personalizat

Iframe-ul e cross-origin, deci CSS-ul din pagina gazdă nu îl afectează. Pasează `customCss` ca să injectezi stiluri în widget — de exemplu să mărești font-size-ul general:

```js
sdk.employerTimesheet({
    sessionToken,
    elementId: 'zecimo-timesheet',
    customCss: 'html { font-size: 16px }',
});
```

## API

### `new zecimoSdk(options?)`

| Opțiune  | Tip      | Default                   | Descriere                                                    |
| -------- | -------- | ------------------------- | ------------------------------------------------------------ |
| `sdkUrl` | `string` | `https://embed.zecimo.ro` | Origin-ul aplicației de embed (override pentru dev/staging). |

### `sdk.timesheet(options): EmbeddedWidget`

Montează widget-ul de pontaj într-un container din pagina părinte.

| Opțiune        | Tip                              | Obligatoriu | Descriere                                                                                                                                                                                                                                                                                                                                                         |
| -------------- | -------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sessionToken` | `string`                         | da          | Token-ul obținut de la `POST /developer/embed/session`.                                                                                                                                                                                                                                                                                                           |
| `elementId`    | `string`                         | da          | ID-ul `<div>`-ului care va găzdui iframe-ul.                                                                                                                                                                                                                                                                                                                      |
| `height`       | `number \| string`               | nu          | Înălțime fixă a iframe-ului. Implicit: **auto-resize** la conținut (min 450px). `number` → px (`600` → `600px`); `string` → valoare CSS (`'100%'`, `'80vh'`, `'600px'`). La înălțime fixă, auto-resize-ul e dezactivat, widget-ul **umple** spațiul (footer fix + scroll intern). Pentru `'100%'`, containerul (`elementId`) trebuie să aibă o înălțime definită. |
| `theme`        | `'light' \| 'dark'`              | nu          | Paleta widgetului. Implicit `'light'`. Aplicată înainte de bootstrap, ca să nu apară o clipire albă într-o pagină întunecată.                                                                                                                                                                                                                                     |
| `customCss`    | `string`                         | nu          | CSS injectat în documentul din iframe (host-ul nu poate stila direct iframe-ul cross-origin). Aplicat ca `<style>` la finalul `<head>`, deci suprascrie stilurile widget-ului la specificitate egală. Ex: `'html { font-size: 16px }'` mărește font-size-ul general.                                                                                              |
| `onEvent`      | `(e: zecimoWidgetEvent) => void` | nu          | Callback pentru evenimente — vezi mai jos.                                                                                                                                                                                                                                                                                                                        |

Returnează un handle:

```ts
interface EmbeddedWidget {
    unmount(): void;
}
```

### `sdk.employerTimesheet(options): EmbeddedWidget`

Montează wizard-ul **Pontaj Angajator** (varianta prietenoasă, pentru administratorul firmei). Acceptă **exact aceleași opțiuni** ca `sdk.timesheet` (`sessionToken`, `elementId`, `height`, `onEvent`) și returnează același handle `EmbeddedWidget`.

### `sdk.employees(options): EmbeddedWidget`

Montează lista **Angajați** (read-only). Acceptă **exact aceleași opțiuni** ca `sdk.timesheet` (`sessionToken`, `elementId`, `height`, `theme`, `customCss`, `onEvent`) și returnează același handle `EmbeddedWidget`. Emite `ready` după încărcare și `error` la eșec.

## Evenimente

```ts
type zecimoWidgetEvent =
    | { type: 'ready' }
    | { type: 'resize'; payload: { height: number } }
    | { type: 'saved' }
    | { type: 'submitted' }
    | { type: 'error'; payload: { message: string } };
```

| Eveniment   | Când se emite                                                                                                           |
| ----------- | ----------------------------------------------------------------------------------------------------------------------- |
| `ready`     | După prima încărcare reușită a pontajului.                                                                              |
| `resize`    | La fiecare schimbare de înălțime — doar în modul auto-resize (când nu se pasează `height`). SDK-ul ajustează iframe-ul. |
| `saved`     | Pontajul a fost salvat ca draft (la `timesheet` manual; la `employerTimesheet` automat, la fiecare schimbare de pas).   |
| `submitted` | Utilizatorul a închis luna (status → `submitted`).                                                                      |
| `error`     | Eșec la încărcare sau autentificare.                                                                                    |

## Comportamentul widget-urilor

### `timesheet` (contabil)

- **Selector de lună** integrat — utilizatorul navighează între luni cu `<` / `>`.
- Default este **luna trecută** (cazul tipic pentru pontaj).
- **Auto-create** — dacă nu există pontaj pentru luna selectată, se creează automat la prima încărcare.
- **Editabil** până la închidere. La `status === 'submitted'`, formularul devine read-only și butonul _Închide_ dispare.
- **Salvează** și **Închide** sunt expuse în header. Reopen-ul nu este disponibil în embed (acțiune de admin în aplicația principală).

### `employerTimesheet` (angajator)

- **Ecran de start** cu lista pontajelor existente pe luni + un card pentru **luna în curs** (default).
- **Wizard în 7 pași**: Prezență (click pe o zi → alegi statusul), Concedii medicale, Ore suplimentare, Tichete, Beneficii, Persoane în întreținere, Verificare.
- Pornește de la ideea că **toți au lucrat normal** — angajatorul marchează doar excepțiile.
- **Validare blocantă** — nu se poate trece la pasul următor până nu e complet/confirmat.
- **Auto-create** la deschiderea unei luni; **autosave** (`status: 'saved'`) la fiecare schimbare de pas — emite `saved`.
- După **Trimite** (`status: 'submitted'`), pontajul e **închis**: se afișează un **rezumat read-only** și nu mai poate fi editat sau redeschis din embed.

### `employees` (angajați)

- Tabel cu angajații firmei: Nume, Prenume, CNP, Tip contract, Salariu brut, Stare contract (Activ / Suspendat / Încetat).
- **Căutare** în tabel (Nume, Prenume, CNP, …), fără taburi de filtrare.
- **Click pe un rând** deschide **fișa angajatului** — exact aceeași componentă ca în aplicația contabilului, cu taburile _Date personale_, _Istoric contracte_, _Timeline_, _Documente_ și _Rețineri salariale_.
- Fișa e **restrânsă** față de cea a contabilului:
    - nimic legat de **REGES** (fără badge-uri de sincronizare, fără trimitere);
    - **fără** tabul _Documente contabile_;
    - datele personale, contractele și versiunile sunt **doar de citit** — nu se pot adăuga, modifica sau șterge contracte/versiuni și nici radia un contract;
    - **se pot încărca documente** noi (fără editare/ștergere) și **se pot adăuga rețineri salariale** (fără modificare/ștergere).
- Buton **Angajat nou** afișat în header (fără acțiune deocamdată). Buton _Înapoi la listă_ deasupra fișei.
- Emite `ready` după încărcare, `error` la eșec; suportă `height` / `theme` / `customCss` ca celelalte widget-uri.

```bash
nx run web-sdk:build         # bundle ESM/CJS/IIFE + declarații TS
nx run sdk:serve             # rulează aplicația host pe :4501
```

Pentru un test cross-origin rapid, `apps/sdk/public/test.html` permite paste-ul unui token și montează oricare widget direct (butoanele **Mount (contabil)** / **Mount (angajator)** / **Mount (angajați)**).

## Documentație extinsă

Vezi [`docs/embed-sdk.md`](../../docs/embed-sdk.md) pentru flow-ul complet, inclusiv exemple de eroare și semantica auditării.
