@tiledesk/tiledesk-server
Version:
The Tiledesk server module
865 lines (648 loc) • 34.2 kB
Markdown
# Activity History — Guida per il client (dashboard)
Documentazione per integrare e visualizzare le **activity** del progetto nella dashboard.
Copre la struttura dati, tutti i `verb` disponibili (legacy e nuovi) e come costruire le frasi da mostrare all'utente.
## Prerequisiti
Le activity vengono archiviate solo se sul server è attivo:
```env
ACTIVITY_HISTORY_ENABLED=true
```
Con `QUEUE_ENABLED=true`, gli eventi chatbot, KB, **invite** e **rimozione teammate** passano dalla coda AMQP (come `request.assigned` e `project_user.update`): l'archiver ascolta le versioni `.queue` (`project_user.invite.queue`, `project_user.delete.queue`, ecc.) e il payload include `req.user` serializzato per ricostruire l'attore.
## API
### Lista activity
```
GET /{project_id}/activities
```
**Query params utili**
| Parametro | Descrizione |
|---|---|
| `page` | Pagina (default `0`) |
| `limit` | Elementi per pagina (default `40`, max `100` in lista, max `1000` con `chart=true`) |
| `chart` | `true` se il client sta visualizzando il grafico/timeline (abilita `limit` > 100, fino a 1000) |
| `direction` | Ordinamento su `createdAt`: `-1` desc (default), `1` asc |
| `agent_id` | Filtra per agente coinvolto (`actor.id` o `target.object.id_user._id`) |
| `activities` | Lista verb separati da virgola, es. `REQUEST_ASSIGNED_AUTO,REQUEST_ASSIGNED_SELF` |
| `start_date` / `end_date` | Filtro data (`DD/MM/YYYY`) |
**Limiti `limit`**
| Modalità | Query | Max `limit` |
|---|---|---|
| Lista (default) | senza `chart` | `100` (valori superiori vengono ridotti a 100) |
| Grafico / timeline | `chart=true` | `1000` |
Se con `chart=true` il client passa `limit` > 1000, il server restituisce al massimo 1000 activity (in base ai filtri) e aggiunge `limitWarning` nella risposta.
**Esempio grafico ultimo mese**
```
GET /{project_id}/activities?chart=true&limit=1000&start_date=01/06/2026&end_date=30/06/2026&direction=1
```
**Risposta**
```json
{
"perPage": 40,
"count": 120,
"activities": [
{
"_id": "...",
"id_project": "...",
"createdAt": "2026-06-22T10:00:00.000Z",
"updatedAt": "2026-06-22T10:00:00.000Z",
"actor": {
"type": "user",
"id": "64a1b2c3...",
"name": "Mario Rossi"
},
"verb": "REQUEST_ASSIGNED_SELF",
"actionObj": { ... },
"target": {
"type": "request",
"id": "...",
"object": { ... }
},
"message": "Mario Rossi joined conversation support-group-abc (source: api)"
}
]
}
```
Campi opzionali nella risposta:
- `chart: true` — presente se la richiesta include `chart=true`
- `limitWarning` — presente solo se `limit` richiesto > 1000 (con `chart=true`); i risultati sono comunque limitati a 1000
> **Nota:** il campo `message` è un **fallback in inglese** generato dal server. È utile per debug e come testo di riserva se manca la traduzione i18n lato client. In produzione la dashboard dovrebbe preferire le proprie traduzioni basate su `verb` + dati strutturati.
### Export CSV
```
GET /{project_id}/activities/csv
```
Stessi filtri della lista. La colonna `message` segue la stessa logica di fallback.
## Modello dati Activity
Ogni activity segue il pattern **actor → verb → target** (Activity Streams).
```ts
interface Activity {
_id: string;
id_project: string;
createdAt: string;
updatedAt: string;
/** Chi ha eseguito l'azione */
actor: {
type: 'user' | 'system';
id: string;
name?: string;
};
/** Tipo di evento — chiave per i18n */
verb: ActivityVerb;
/** Dettagli specifici dell'azione (varia per verb) */
actionObj?: Record<string, unknown>;
/** Oggetto su cui è stata eseguita l'azione */
target: {
type: string;
id: string;
object: Record<string, unknown>;
};
/** Fallback inglese (solo per alcuni verb, vedi sotto) */
message?: string;
}
```
### Ruoli dei campi
| Campo | Significato |
|---|---|
| `actor` | Chi ha **fatto** l'azione (utente loggato, o `system` per azioni automatiche) |
| `verb` | **Cosa** è successo — usare come chiave i18n principale |
| `target` | **Su cosa** — conversazione, project user, invito, ecc. |
| `actionObj` | Metadati aggiuntivi per costruire la frase (assignee, source, role, …) |
## Catalogo verb
### Conversazioni — legacy
| Verb | Quando viene emesso |
|---|---|
| `REQUEST_CREATE` | Nuova conversazione creata dal visitatore |
| `REQUEST_CLOSE` | Conversazione chiusa da un agente |
### Conversazioni — assegnazione (nuovi)
| Verb | Significato | Esempio frase (IT) |
|---|---|---|
| `REQUEST_ASSIGNED_AUTO` | Il **sistema** ha assegnato la conversazione a un agente (round robin, queue, chatbot, creazione) | *All'utente **X** è stata assegnata la conversazione **Y** dal sistema* |
| `REQUEST_ASSIGNED_SELF` | Un agente ha fatto **join/pick** manuale sulla conversazione | *L'utente **X** ha fatto join sulla conversazione **Y*** |
| `REQUEST_ASSIGNED_MANUAL` | Un operatore ha **riassegnato** la conversazione a un altro agente, a un chatbot o a un dipartimento (`assigneeType`: `user` \| `bot` \| `department`) | *Vedi template per tipo in `REQUEST_ASSIGNED_MANUAL`* |
| `REQUEST_UNASSIGNED` | Un agente è stato rimosso dalla conversazione | *L'utente **X** è stato rimosso dalla conversazione **Y*** |
### Team / progetto — legacy
| Verb | Quando viene emesso |
|---|---|
| `PROJECT_USER_INVITE` | Invito di un utente al progetto |
| `PROJECT_USER_UPDATE` | Modifica ruolo, impostazioni o disponibilità di un altro utente (admin → agente) |
| `PROJECT_USER_DELETE` | Rimozione di un utente dal progetto |
### Team — disponibilità (nuovi)
| Verb | Significato | Esempio frase (IT) |
|---|---|---|
| `PROJECT_USER_AVAILABILITY_SELF` | L'agente ha cambiato **il proprio** stato di disponibilità | *L'utente **X** ha modificato il suo stato in **inattivo*** |
| `PROJECT_USER_AVAILABILITY_SYSTEM` | Lo stato è stato cambiato **automaticamente dal sistema** (es. disconnessione via subscription) | *Lo stato dell'utente **X** è stato modificato in **inattivo** dal sistema* |
> `PROJECT_USER_UPDATE` resta in uso per le modifiche fatte da un admin sul profilo di un altro utente (ruolo, disponibilità altrui, impostazioni, ecc.).
### Chatbot (FAQ_KB)
| Verb | Significato | Esempio frase (IT) |
|---|---|---|
| `FAQ_KB_CREATE` | Creazione di un chatbot | *L'utente **X** ha creato il chatbot **Y*** |
| `FAQ_KB_DELETE` | Eliminazione di un chatbot | *L'utente **X** ha eliminato il chatbot **Y*** |
| `FAQ_KB_PUBLISH` | Pubblicazione di un chatbot | *L'utente **X** ha pubblicato il chatbot **Y*** |
> **Publish vs fork interno:** la publish fork-a il chatbot in uno snapshot `trashed` (non visibile in UI). Quel fork **non** genera `FAQ_KB_CREATE` — solo `FAQ_KB_PUBLISH` sul chatbot originale. Il fork manuale (`POST /faq_kb/fork/:id` senza `for_publish=true`) continua a generare `FAQ_KB_CREATE`.
`target.type` = `faq_kb` — nome in `target.object.name` o `actionObj.name`.
### Knowledge Base — namespace e contenuti
| Verb | Significato | Esempio frase (IT) |
|---|---|---|
| `KB_NAMESPACE_CREATE` | Creazione di un nuovo namespace | *L'utente **X** ha creato il namespace **Y*** |
| `KB_NAMESPACE_DELETE` | Eliminazione completa di un namespace (namespace + contenuti) | *L'utente **X** ha eliminato il namespace **Y*** |
| `KB_CONTENTS_ADD` | Aggiunta di contenuti a un namespace | *L'utente **X** ha aggiunto … al namespace **Y*** |
| `KB_CONTENTS_DELETE` | Eliminazione di tutti i contenuti di un namespace (il namespace resta) | *L'utente **X** ha eliminato tutti i contenuti dal namespace **Y*** |
`target.type` = `kb_namespace` — nome in `target.object.name` o `actionObj.namespaceName`.
Per `KB_CONTENTS_ADD`, il tipo di aggiunta è in `actionObj.contentAddType`:
| `contentAddType` | Endpoint API | Descrizione |
|---|---|---|
| `content` | `POST /kb/` | Singolo contenuto (testo, URL, ecc.) |
| `url_list` | `POST /kb/multi` | Lista di URL |
| `csv` | `POST /kb/csv` | Import da file CSV |
| `sitemap` | `POST /kb/sitemap/import` | Import da sitemap |
## `actionObj` per i verb di disponibilità
Per `PROJECT_USER_AVAILABILITY_SELF` e `PROJECT_USER_AVAILABILITY_SYSTEM`:
```ts
interface AvailabilityActionObj {
/** Valore inviato nel body della PUT */
user_available?: boolean;
profileStatus?: string;
/** Etichetta normalizzata del nuovo stato — usare per i18n */
newStatus?: string; // es. "available", "unavailable", "inactive"
/** Tipo semantico (ridondante con verb) */
updateType: 'self' | 'system' | 'admin';
source: 'api' | 'subscription' | 'system';
}
```
> **Nota:** lo stato precedente non è incluso nell'activity. Il server legge i valori `previous` prima dell'update ma, soprattutto con job worker/queue attivo, questa informazione non era affidabilmente disponibile nell'archiver. Le frasi UI usano solo il **nuovo stato** (`newStatus`).
### Come distinguere self vs system
| Campo | Self | System |
|---|---|---|
| `verb` | `PROJECT_USER_AVAILABILITY_SELF` | `PROJECT_USER_AVAILABILITY_SYSTEM` |
| `actor.type` | `user` | `system` |
| `actor.id` | `id_user` di chi ha cambiato stato | `system` |
| `target.id` | `project_user_id` dell'agente | `project_user_id` dell'agente |
| `actionObj.updateType` | `self` | `system` |
| `actionObj.source` | `api` | `subscription` |
**Logica server (dopo la correzione):**
1. **System** se `req.user` è una **Subscription** (JWT `sub: subscription`, oppure documento con `event` + `target` + `id_project`)
2. **System** se il body contiene `"availabilityInitiator": "system"` (vedi sotto)
3. **Self** se l'utente chiama `PUT /project_users/` (aggiornamento del proprio profilo)
4. **Self** se `actor.id` coincide con `target.object.id_user._id` (confronto normalizzato)
5. **Safety net in archiver:** se `verb` è `SELF` ma `actor.id ≠ target.id_user` → riclassificato come `PROJECT_USER_UPDATE` o `SYSTEM` (se subscription)
> **Caso tipico di errore (evento 1):** la subscription chiama l'API con JWT utente su `PUT /project_users/` oppure `req.user` è il documento Subscription ma non viene riconosciuto (`instanceof` fallito). In entrambi i casi il vecchio codice marcava `SELF` in modo errato.
### Disconnessione da dashboard (subscription client)
Se il client di disconnessione usa il **token utente** (non JWT subscription) su `PUT /project_users/`, passare esplicitamente:
```json
{
"user_available": false,
"profileStatus": "inactive",
"availabilityInitiator": "system"
}
```
In alternativa usare JWT **subscription** su `PUT /project_users/:project_userid` — viene classificato automaticamente come system.
> **Nota legacy:** nelle activity archiviate prima di questa correzione, `actor.id` poteva contenere l'id della subscription con `actor.type: "user"`. Per i record legacy: se `actor.id` non corrisponde a `target.object.id_user._id` e il `verb` è `SELF`, trattare come system.
### Template frasi — disponibilità
**`PROJECT_USER_AVAILABILITY_SELF`**
| Lingua | Template |
|---|---|
| **IT** | `{{targetUser}} ha modificato il suo stato in {{newStatus}}` |
| **EN** | `{{targetUser}} changed availability status to {{newStatus}}` |
**`PROJECT_USER_AVAILABILITY_SYSTEM`**
| Lingua | Template |
|---|---|
| **IT** | `Lo stato di {{targetUser}} è stato modificato in {{newStatus}} dal sistema` |
| **EN** | `{{targetUser}} availability status was changed to {{newStatus}} by the system` |
`targetUser` si ricava da `target.object.id_user` (firstname + lastname).
### Valori di `newStatus`
| Valore | Significato |
|---|---|
| `available` | `user_available: true` |
| `unavailable` | `user_available: false` |
| valore di `profileStatus` | es. `inactive`, `away`, … (se presente ha priorità su `user_available` nel label server) |
## `actionObj` per chatbot e Knowledge Base
### Chatbot (`FAQ_KB_*`)
```ts
interface FaqKbActionObj {
name?: string;
type?: string;
subtype?: string;
publishedBotId?: string; // solo FAQ_KB_PUBLISH
release_note?: string; // solo FAQ_KB_PUBLISH
}
```
### Namespace e contenuti (`KB_*`)
```ts
interface KbNamespaceActionObj {
namespaceName?: string;
hybrid?: boolean;
default?: boolean; // KB_NAMESPACE_CREATE
deletedCount?: number; // KB_NAMESPACE_DELETE, KB_CONTENTS_DELETE
}
interface KbContentsAddActionObj {
contentAddType: 'content' | 'url_list' | 'csv' | 'sitemap';
namespaceName?: string;
count?: number;
type?: string; // tipo KB (txt, url, sitemap, …) — solo content/sitemap
source?: string; // URL o nome sorgente
}
interface KbContentsDeleteActionObj {
namespaceName?: string;
deletedCount?: number;
deleteMode: 'contents_only';
}
```
### Template frasi — chatbot e KB
| Verb | IT | EN |
|---|---|---|
| `FAQ_KB_CREATE` | `{{actor}} ha creato il chatbot {{chatbot}}` | `{{actor}} created chatbot {{chatbot}}` |
| `FAQ_KB_DELETE` | `{{actor}} ha eliminato il chatbot {{chatbot}}` | `{{actor}} deleted chatbot {{chatbot}}` |
| `FAQ_KB_PUBLISH` | `{{actor}} ha pubblicato il chatbot {{chatbot}}` | `{{actor}} published chatbot {{chatbot}}` |
| `KB_NAMESPACE_CREATE` | `{{actor}} ha creato il namespace {{namespace}}` | `{{actor}} created namespace {{namespace}}` |
| `KB_NAMESPACE_DELETE` | `{{actor}} ha eliminato il namespace {{namespace}}` | `{{actor}} deleted namespace {{namespace}}` |
| `KB_CONTENTS_ADD` (singolo) | `{{actor}} ha aggiunto un contenuto al namespace {{namespace}}` | `{{actor}} added content to namespace {{namespace}}` |
| `KB_CONTENTS_ADD` (multi) | `{{actor}} ha aggiunto {{count}} elementi ({{addType}}) al namespace {{namespace}}` | `{{actor}} added {{count}} items ({{addType}}) to namespace {{namespace}}` |
| `KB_CONTENTS_DELETE` | `{{actor}} ha eliminato tutti i contenuti dal namespace {{namespace}}` | `{{actor}} deleted all contents from namespace {{namespace}}` |
Per `KB_CONTENTS_ADD` con `contentAddType: 'sitemap'`, includere opzionalmente `{{source}}` (URL della sitemap).
## `actionObj` per i verb di assegnazione
Per i verb `REQUEST_ASSIGNED_*` e `REQUEST_UNASSIGNED`, `actionObj` ha questa forma:
```ts
interface AssignmentActionObj {
/** id dell'assegnatario: id_user (agente), id chatbot o id dipartimento */
assigneeId: string;
/** Nome leggibile dell'assegnatario (agente, chatbot o dipartimento) */
assigneeName?: string;
/** Tipo di assegnatario */
assigneeType?: 'user' | 'bot' | 'department';
/** Tipo semantico (ridondante con verb, utile per debug) */
assignmentType: 'auto' | 'self_join' | 'manual_reassign' | 'manual_reassign_bot' | 'manual_reassign_department' | 'unassign';
/** Origine tecnica dell'evento */
source: 'api' | 'subscription' | 'chatbot' | 'queue' | 'webhook' | 'rules' | 'create' | 'system';
/** id_user dell'agente precedente (solo su reassign) */
previousAssigneeId?: string | null;
/** Partecipanti rimossi (solo su reassign/unassign) */
removedParticipants?: string[];
}
```
### Valori di `source`
| Source | Descrizione |
|---|---|
| `api` | Chiamata API dalla dashboard (utente reale) |
| `subscription` | Client automatico con JWT subscription (disconnessione, cleanup partecipanti, …) |
| `chatbot` | Handover dal chatbot (`PUT /agent`) |
| `queue` | Smart assignment / reroute automatico in coda |
| `webhook` | Sincronizzazione Chat21 (`join-member`) |
| `rules` | Trigger / regole automatiche |
| `create` | Assegnazione alla creazione della conversazione |
| `system` | Altri processi interni |
### `actor` vs `assigneeId`
| Scenario | `actor` | `assigneeId` |
|---|---|---|
| Join manuale | L'agente che fa join (`actor.id === assigneeId`) | Stesso agente |
| Assegnazione automatica | `system` oppure l'utente che ha **scatenato** il routing | L'agente scelto dal round robin |
| Reassign manuale | Il supervisore/agente che riassegna | Il nuovo agente assegnato |
| Unassign | Chi ha rimosso il partecipante | L'agente rimosso |
> **Importante:** `assigneeId` è sempre un **`id_user`** (ID utente Tiledesk), non un `project_user._id`.
## Come costruire le frasi (i18n)
### Strategia consigliata
```
1. Leggi activity.verb
2. Risolvi i nomi da actor, actionObj, target
3. Applica il template i18n corrispondente
4. Se il template non esiste → usa activity.message (fallback inglese)
```
### Risoluzione nomi
```ts
function actorName(activity: Activity): string {
if (activity.actor?.type === 'system') return t('ACTIVITY.SYSTEM'); // es. "Sistema"
return activity.actor?.name || activity.actor?.id || t('ACTIVITY.SOMEONE');
}
function conversationLabel(activity: Activity): string {
const request = activity.target?.object;
// Preferire un titolo leggibile se disponibile in futuro; oggi:
return request?.request_id || activity.target?.id || t('ACTIVITY.CONVERSATION');
}
function resolveAgentName(activity: Activity, userId?: string | null): string {
if (!userId) return t('ACTIVITY.UNKNOWN_AGENT');
const agents = activity.target?.object?.participatingAgents;
if (Array.isArray(agents)) {
for (const agent of agents) {
const user = agent.id_user || agent;
const id = String(user._id || user.id || user);
if (id === String(userId)) {
const name = [user.firstname, user.lastname].filter(Boolean).join(' ').trim();
if (name) return name;
}
}
}
// Fallback: cercare nel team del progetto se già in cache
return userId;
}
```
---
## Template frasi — assegnazione conversazioni
### `REQUEST_ASSIGNED_SELF`
L'utente ha fatto join sulla conversazione.
| Lingua | Template |
|---|---|
| **IT** | `{{actor}} ha fatto join sulla conversazione {{conversation}}` |
| **EN** | `{{actor}} joined conversation {{conversation}}` |
```ts
// actor === assignee
const actor = actorName(activity);
const conversation = conversationLabel(activity);
// IT: "Mario Rossi ha fatto join sulla conversazione support-group-abc"
```
### `REQUEST_ASSIGNED_AUTO`
La conversazione è stata assegnata automaticamente dal sistema.
Due varianti in base a `actor.type`:
| Condizione | IT | EN |
|---|---|---|
| `actor.type === 'system'` | `All'utente **{{assignee}}** è stata assegnata la conversazione **{{conversation}}** dal sistema` | `Conversation **{{conversation}}** was automatically assigned to **{{assignee}}** by the system` |
| `actor.type === 'user'` | `All'utente **{{assignee}}** è stata assegnata la conversazione **{{conversation}}** (assegnazione automatica avviata da **{{actor}}**)` | `Conversation **{{conversation}}** was automatically assigned to **{{assignee}}** (triggered by **{{actor}}**)` |
```ts
const assignee = resolveAgentName(activity, activity.actionObj?.assigneeId);
const conversation = conversationLabel(activity);
const actor = actorName(activity);
if (activity.actor?.type === 'system') {
// "All'utente Mario Rossi è stata assegnata la conversazione support-group-abc dal sistema"
} else {
// "All'utente Mario Rossi è stata assegnata la conversazione support-group-abc (assegnazione automatica avviata da Laura Bianchi)"
}
```
### `REQUEST_ASSIGNED_MANUAL`
Un operatore ha assegnato la conversazione a un altro agente, a un chatbot o a un dipartimento.
Usare `actionObj.assigneeType` per distinguere i tre casi. Se presente, preferire `actionObj.assigneeName` al posto della risoluzione da `assigneeId`.
| `assigneeType` | IT | EN |
|---|---|---|
| `user` (default) | `All'utente **{{assignee}}** è stata assegnata la conversazione **{{conversation}}** da **{{actor}}**` | `**{{actor}}** assigned conversation **{{conversation}}** to **{{assignee}}**` |
| `bot` | `**{{actor}}** ha riassegnato la conversazione **{{conversation}}** al chatbot **{{assignee}}**` | `**{{actor}}** reassigned conversation **{{conversation}}** to chatbot **{{assignee}}**` |
| `department` | `**{{actor}}** ha riassegnato la conversazione **{{conversation}}** al dipartimento **{{assignee}}**` | `**{{actor}}** reassigned conversation **{{conversation}}** to department **{{assignee}}**` |
**Con agente precedente** (`previousAssigneeId` presente, tipico su reassign):
| `assigneeType` | IT | EN |
|---|---|---|
| `user` | `All'utente **{{assignee}}** è stata assegnata la conversazione **{{conversation}}** da **{{actor}}** (sostituisce **{{previous}}**)` | `**{{actor}}** reassigned conversation **{{conversation}}** to **{{assignee}}** (replacing **{{previous}}**)` |
| `bot` / `department` | Aggiungere `(sostituisce **{{previous}}**)` / `(replacing **{{previous}}**)` | stesso pattern |
```ts
const actor = actorName(activity);
const assignee = activity.actionObj?.assigneeName || resolveAssigneeName(activity);
const assigneeType = activity.actionObj?.assigneeType || 'user';
const previous = resolveAgentName(activity, activity.actionObj?.previousAssigneeId);
const conversation = conversationLabel(activity);
// IT bot: "Laura Bianchi ha riassegnato la conversazione support-group-abc al chatbot Support Bot"
// IT department: "Laura Bianchi ha riassegnato la conversazione support-group-abc al dipartimento Vendite"
```
### `REQUEST_UNASSIGNED`
| Lingua | Template |
|---|---|
| **IT** | `**{{actor}}** ha rimosso **{{assignee}}** dalla conversazione **{{conversation}}**` |
| **EN** | `**{{actor}}** unassigned **{{assignee}}** from conversation **{{conversation}}**` |
**Via subscription** (`actionObj.source === 'subscription'`, tipico disconnessione agente):
| Lingua | Template |
|---|---|
| **IT** | `**{{assignee}}** è stato rimosso dalla conversazione **{{conversation}}** dal sistema` |
| **EN** | `**{{assignee}}** was unassigned from conversation **{{conversation}}** by the system` |
In questo caso `actor.type` = `system`, `actor.id` = `system` (non l'id della subscription).
> **Record legacy:** se `actor.type === 'user'` ma `actor.id` è un id subscription, trattare come `source: subscription` e actor = System.
## Template frasi — conversazioni legacy
### `REQUEST_CREATE`
| Lingua | Template |
|---|---|
| **IT** | `**{{actor}}** ha avviato una nuova conversazione` |
| **EN** | `**{{actor}}** started a new conversation` |
`actor` = visitatore (`requester_id` / `requester_name`).
`target.object` contiene la request completa (`first_text`, `request_id`, …).
### `REQUEST_CLOSE`
| Lingua | Template |
|---|---|
| **IT** | `**{{actor}}** ha chiuso la conversazione **{{conversation}}**` |
| **EN** | `**{{actor}}** closed conversation **{{conversation}}**` |
`actor.id` = `closed_by`, `actor.name` = `closed_by_name`.
## Template frasi — team legacy
### `PROJECT_USER_INVITE`
Invito di un teammate al progetto (`POST /project_users/invite`). Copre sia utenti già registrati sia inviti in pending (email non ancora su Tiledesk).
| Lingua | Template |
|---|---|
| **IT** | `**{{actor}}** ha invitato **{{target}}** ({{email}}) ad assumere il ruolo di **{{role}}**` |
| **EN** | `**{{actor}}** invited **{{target}}** ({{email}}) to take on the role of **{{role}}**` |
Dati da usare:
- `actor.name` (da `activityActorUtil` / `req.user`)
- `actionObj.email`, `actionObj.role`
- `actionObj.inviteType`: `'registered'` (utente esistente) | `'pending'` (pending invitation)
- `target.type`: `pendinginvitation` vs `project_user`
- `target.object.id_user.firstname/lastname` per utenti registrati, oppure `target.object.email` per pending
```ts
const actor = actorName(activity);
const target = inviteTargetLabel(activity); // nome o email
const email = activity.actionObj?.email || target;
const role = activity.actionObj?.role || 'agent';
```
### `PROJECT_USER_DELETE`
Rimozione di un teammate dal progetto (`DELETE /project_users/:id?soft=true` o `?hard=true`).
| Lingua | Template |
|---|---|
| **IT** | `**{{actor}}** ha rimosso **{{target}}** dal progetto` |
| **EN** | `**{{actor}}** removed **{{target}}** from the project` |
Dati da usare:
- `actor.name`
- `target.object.id_user.firstname/lastname` (o `actionObj.email` come fallback)
- `actionObj.deleteType`: `'soft'` | `'hard'`
- `actionObj.role`: ruolo dell'utente rimosso
```ts
const actor = actorName(activity);
const target = targetUserLabel(activity);
// IT: "Laura Bianchi ha rimosso Mario Rossi dal progetto"
```
### `PROJECT_USER_UPDATE`
Due sotto-casi:
**A) L'utente modifica se stesso** (`actor.id === target.object.id_user._id`)
| Campo cambiato | IT | EN |
|---|---|---|
| `actionObj.user_available === true` | `{{actor}} ha cambiato il suo stato in disponibile` | `{{actor}} changed his status to available` |
| `actionObj.user_available === false` | `{{actor}} ha cambiato il suo stato in non disponibile` | `{{actor}} changed his status to unavailable` |
**B) Un admin modifica un altro utente**
| Campo cambiato | IT | EN |
|---|---|---|
| `actionObj.user_available` | `{{actor}} ha cambiato lo stato di disponibilità di {{target}} in disponibile/non disponibile` | `{{actor}} changed the availability status of {{target}} to available/unavailable` |
| `actionObj.role === 'admin'` | `{{actor}} ha cambiato il ruolo di {{target}} in Amministratore` | `{{actor}} changed the role of {{target}} to Administrator` |
| `actionObj.role === 'agent'` | `{{actor}} ha cambiato il ruolo di {{target}} in Agente` | `{{actor}} changed the role of {{target}} to Agent` |
## Chiavi i18n suggerite
```json
{
"ACTIVITY": {
"SYSTEM": "Sistema",
"SOMEONE": "Qualcuno",
"CONVERSATION": "conversazione",
"UNKNOWN_AGENT": "agente sconosciuto",
"REQUEST_ASSIGNED_SELF": "{{actor}} ha fatto join sulla conversazione {{conversation}}",
"REQUEST_ASSIGNED_AUTO_SYSTEM": "All'utente {{assignee}} è stata assegnata la conversazione {{conversation}} dal sistema",
"REQUEST_ASSIGNED_AUTO_TRIGGERED": "All'utente {{assignee}} è stata assegnata la conversazione {{conversation}} (assegnazione automatica avviata da {{actor}})",
"REQUEST_ASSIGNED_MANUAL": "All'utente {{assignee}} è stata assegnata la conversazione {{conversation}} da {{actor}}",
"REQUEST_ASSIGNED_MANUAL_REPLACED": "All'utente {{assignee}} è stata assegnata la conversazione {{conversation}} da {{actor}} (sostituisce {{previous}})",
"REQUEST_ASSIGNED_MANUAL_BOT": "{{actor}} ha riassegnato la conversazione {{conversation}} al chatbot {{assignee}}",
"REQUEST_ASSIGNED_MANUAL_BOT_REPLACED": "{{actor}} ha riassegnato la conversazione {{conversation}} al chatbot {{assignee}} (sostituisce {{previous}})",
"REQUEST_ASSIGNED_MANUAL_DEPARTMENT": "{{actor}} ha riassegnato la conversazione {{conversation}} al dipartimento {{assignee}}",
"REQUEST_ASSIGNED_MANUAL_DEPARTMENT_REPLACED": "{{actor}} ha riassegnato la conversazione {{conversation}} al dipartimento {{assignee}} (sostituisce {{previous}})",
"REQUEST_UNASSIGNED": "{{actor}} ha rimosso {{assignee}} dalla conversazione {{conversation}}",
"REQUEST_UNASSIGNED_SYSTEM": "{{assignee}} è stato rimosso dalla conversazione {{conversation}} dal sistema",
"PROJECT_USER_AVAILABILITY_SELF": "{{targetUser}} ha modificato il suo stato in {{newStatus}}",
"PROJECT_USER_AVAILABILITY_SYSTEM": "Lo stato di {{targetUser}} è stato modificato in {{newStatus}} dal sistema",
"FAQ_KB_CREATE": "{{actor}} ha creato il chatbot {{chatbot}}",
"FAQ_KB_DELETE": "{{actor}} ha eliminato il chatbot {{chatbot}}",
"FAQ_KB_PUBLISH": "{{actor}} ha pubblicato il chatbot {{chatbot}}",
"KB_NAMESPACE_CREATE": "{{actor}} ha creato il namespace {{namespace}}",
"KB_NAMESPACE_DELETE": "{{actor}} ha eliminato il namespace {{namespace}}",
"KB_CONTENTS_ADD": "{{actor}} ha aggiunto contenuti al namespace {{namespace}}",
"KB_CONTENTS_ADD_MULTI": "{{actor}} ha aggiunto {{count}} elementi ({{addType}}) al namespace {{namespace}}",
"KB_CONTENTS_DELETE": "{{actor}} ha eliminato tutti i contenuti dal namespace {{namespace}}",
"REQUEST_CREATE": "{{actor}} ha avviato una nuova conversazione",
"REQUEST_CLOSE": "{{actor}} ha chiuso la conversazione {{conversation}}",
"PROJECT_USER_INVITE": "{{actor}} ha invitato {{target}} ({{email}}) ad assumere il ruolo di {{role}}",
"PROJECT_USER_DELETE": "{{actor}} ha rimosso {{target}} dal progetto",
"PROJECT_USER_UPDATE_SELF_AVAILABLE": "{{actor}} ha cambiato il suo stato in disponibile",
"PROJECT_USER_UPDATE_SELF_UNAVAILABLE": "{{actor}} ha cambiato il suo stato in non disponibile",
"PROJECT_USER_UPDATE_AVAILABILITY": "{{actor}} ha cambiato lo stato di disponibilità di {{target}}",
"PROJECT_USER_UPDATE_ROLE_ADMIN": "{{actor}} ha cambiato il ruolo di {{target}} in Amministratore",
"PROJECT_USER_UPDATE_ROLE_AGENT": "{{actor}} ha cambiato il ruolo di {{target}} in Agente"
}
}
```
## Funzione di rendering consigliata (pseudocodice)
```ts
function renderActivity(activity: Activity, t: TranslateFn): string {
// 1. Fallback server
if (!hasTranslation(activity.verb)) {
return activity.message || activity.verb;
}
const actor = actorName(activity);
const conversation = conversationLabel(activity);
const assignee = resolveAgentName(activity, activity.actionObj?.assigneeId);
const previous = resolveAgentName(activity, activity.actionObj?.previousAssigneeId);
switch (activity.verb) {
case 'REQUEST_ASSIGNED_SELF':
return t('ACTIVITY.REQUEST_ASSIGNED_SELF', { actor, conversation });
case 'REQUEST_ASSIGNED_AUTO':
return activity.actor?.type === 'system'
? t('ACTIVITY.REQUEST_ASSIGNED_AUTO_SYSTEM', { assignee, conversation })
: t('ACTIVITY.REQUEST_ASSIGNED_AUTO_TRIGGERED', { assignee, conversation, actor });
case 'REQUEST_ASSIGNED_MANUAL': {
const assigneeType = activity.actionObj?.assigneeType || 'user';
const resolvedAssignee = activity.actionObj?.assigneeName || assignee;
if (assigneeType === 'bot') {
return activity.actionObj?.previousAssigneeId
? t('ACTIVITY.REQUEST_ASSIGNED_MANUAL_BOT_REPLACED', { assignee: resolvedAssignee, conversation, actor, previous })
: t('ACTIVITY.REQUEST_ASSIGNED_MANUAL_BOT', { assignee: resolvedAssignee, conversation, actor });
}
if (assigneeType === 'department') {
return activity.actionObj?.previousAssigneeId
? t('ACTIVITY.REQUEST_ASSIGNED_MANUAL_DEPARTMENT_REPLACED', { assignee: resolvedAssignee, conversation, actor, previous })
: t('ACTIVITY.REQUEST_ASSIGNED_MANUAL_DEPARTMENT', { assignee: resolvedAssignee, conversation, actor });
}
return activity.actionObj?.previousAssigneeId
? t('ACTIVITY.REQUEST_ASSIGNED_MANUAL_REPLACED', { assignee: resolvedAssignee, conversation, actor, previous })
: t('ACTIVITY.REQUEST_ASSIGNED_MANUAL', { assignee: resolvedAssignee, conversation, actor });
}
case 'REQUEST_UNASSIGNED':
return activity.actionObj?.source === 'subscription'
? t('ACTIVITY.REQUEST_UNASSIGNED_SYSTEM', { assignee, conversation })
: t('ACTIVITY.REQUEST_UNASSIGNED', { actor, assignee, conversation });
case 'REQUEST_CREATE':
return t('ACTIVITY.REQUEST_CREATE', { actor });
case 'REQUEST_CLOSE':
return t('ACTIVITY.REQUEST_CLOSE', { actor, conversation });
// ... PROJECT_USER_* come da tabella sopra
default:
return activity.message || activity.verb;
}
}
```
## Esempi completi
### Join manuale
```json
{
"verb": "REQUEST_ASSIGNED_SELF",
"actor": { "type": "user", "id": "user_abc", "name": "Mario Rossi" },
"actionObj": {
"assigneeId": "user_abc",
"assignmentType": "self_join",
"source": "api"
},
"target": {
"type": "request",
"object": { "request_id": "support-group-xyz" }
}
}
```
→ **IT:** `Mario Rossi ha fatto join sulla conversazione support-group-xyz`
### Assegnazione automatica (sistema)
```json
{
"verb": "REQUEST_ASSIGNED_AUTO",
"actor": { "type": "system", "id": "system", "name": "System" },
"actionObj": {
"assigneeId": "user_abc",
"assignmentType": "auto",
"source": "queue"
},
"target": {
"type": "request",
"object": {
"request_id": "support-group-xyz",
"participatingAgents": [
{ "id_user": { "_id": "user_abc", "firstname": "Mario", "lastname": "Rossi" } }
]
}
}
}
```
→ **IT:** `All'utente Mario Rossi è stata assegnata la conversazione support-group-xyz dal sistema`
### Reassign manuale
```json
{
"verb": "REQUEST_ASSIGNED_MANUAL",
"actor": { "type": "user", "id": "user_supervisor", "name": "Laura Bianchi" },
"actionObj": {
"assigneeId": "user_abc",
"assignmentType": "manual_reassign",
"source": "api",
"previousAssigneeId": "user_old"
},
"target": {
"type": "request",
"object": { "request_id": "support-group-xyz" }
}
}
```
→ **IT:** `All'utente Mario Rossi è stata assegnata la conversazione support-group-xyz da Laura Bianchi`
## Filtri dashboard consigliati
Per una vista "Assegnazioni conversazioni":
```
GET /{project_id}/activities?activities=REQUEST_ASSIGNED_AUTO,REQUEST_ASSIGNED_SELF,REQUEST_ASSIGNED_MANUAL,REQUEST_UNASSIGNED
```
Per storico completo includere anche `REQUEST_CREATE` e `REQUEST_CLOSE`.
Per attività chatbot e Knowledge Base:
```
GET /{project_id}/activities?activities=FAQ_KB_CREATE,FAQ_KB_DELETE,FAQ_KB_PUBLISH,KB_NAMESPACE_CREATE,KB_NAMESPACE_DELETE,KB_CONTENTS_ADD,KB_CONTENTS_DELETE
```
Per attività di un singolo agente:
```
GET /{project_id}/activities?agent_id={user_id}
```
## Note implementative
1. **`target.object` può essere grande** — contiene la request snapshot al momento dell'evento. Per la UI usare solo i campi necessari (`request_id`, `participatingAgents`, `first_text`).
2. **Non usare l'endpoint HTTP** per capire il tipo di assegnazione — usare sempre `verb` e `actionObj.assignmentType`.
3. **`actor.id` su `PROJECT_USER_UPDATE`** può essere `_id` MongoDB, mentre su altri eventi è `user.id` — confrontare sempre come stringa.
4. **Preflight** — le request con `preflight: true` non generano activity di creazione.
5. Il campo `message` dalla API è **solo inglese** e copre i verb di assegnazione, disponibilità, chatbot e Knowledge Base; per gli altri verb legacy il client deve costruire la frase localmente.