@tiledesk/tiledesk-server 2.19.13 → 2.19.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,864 @@
1
+ # Activity History — Guida per il client (dashboard)
2
+
3
+ Documentazione per integrare e visualizzare le **activity** del progetto nella dashboard.
4
+ Copre la struttura dati, tutti i `verb` disponibili (legacy e nuovi) e come costruire le frasi da mostrare all'utente.
5
+
6
+ ---
7
+
8
+ ## Prerequisiti
9
+
10
+ Le activity vengono archiviate solo se sul server è attivo:
11
+
12
+ ```env
13
+ ACTIVITY_HISTORY_ENABLED=true
14
+ ```
15
+
16
+ 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.
17
+
18
+ ---
19
+
20
+ ## API
21
+
22
+ ### Lista activity
23
+
24
+ ```
25
+ GET /{project_id}/activities
26
+ ```
27
+
28
+ **Query params utili**
29
+
30
+ | Parametro | Descrizione |
31
+ |---|---|
32
+ | `page` | Pagina (default `0`) |
33
+ | `limit` | Elementi per pagina (default `40`, max `100` in lista, max `1000` con `chart=true`) |
34
+ | `chart` | `true` se il client sta visualizzando il grafico/timeline (abilita `limit` > 100, fino a 1000) |
35
+ | `direction` | Ordinamento su `createdAt`: `-1` desc (default), `1` asc |
36
+ | `agent_id` | Filtra per agente coinvolto (`actor.id` o `target.object.id_user._id`) |
37
+ | `activities` | Lista verb separati da virgola, es. `REQUEST_ASSIGNED_AUTO,REQUEST_ASSIGNED_SELF` |
38
+ | `start_date` / `end_date` | Filtro data (`DD/MM/YYYY`) |
39
+
40
+ **Limiti `limit`**
41
+
42
+ | Modalità | Query | Max `limit` |
43
+ |---|---|---|
44
+ | Lista (default) | senza `chart` | `100` (valori superiori vengono ridotti a 100) |
45
+ | Grafico / timeline | `chart=true` | `1000` |
46
+
47
+ 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.
48
+
49
+ **Esempio grafico ultimo mese**
50
+
51
+ ```
52
+ GET /{project_id}/activities?chart=true&limit=1000&start_date=01/06/2026&end_date=30/06/2026&direction=1
53
+ ```
54
+
55
+ **Risposta**
56
+
57
+ ```json
58
+ {
59
+ "perPage": 40,
60
+ "count": 120,
61
+ "activities": [
62
+ {
63
+ "_id": "...",
64
+ "id_project": "...",
65
+ "createdAt": "2026-06-22T10:00:00.000Z",
66
+ "updatedAt": "2026-06-22T10:00:00.000Z",
67
+ "actor": {
68
+ "type": "user",
69
+ "id": "64a1b2c3...",
70
+ "name": "Mario Rossi"
71
+ },
72
+ "verb": "REQUEST_ASSIGNED_SELF",
73
+ "actionObj": { ... },
74
+ "target": {
75
+ "type": "request",
76
+ "id": "...",
77
+ "object": { ... }
78
+ },
79
+ "message": "Mario Rossi joined conversation support-group-abc (source: api)"
80
+ }
81
+ ]
82
+ }
83
+ ```
84
+
85
+ Campi opzionali nella risposta:
86
+ - `chart: true` — presente se la richiesta include `chart=true`
87
+ - `limitWarning` — presente solo se `limit` richiesto > 1000 (con `chart=true`); i risultati sono comunque limitati a 1000
88
+
89
+ > **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.
90
+
91
+ ### Export CSV
92
+
93
+ ```
94
+ GET /{project_id}/activities/csv
95
+ ```
96
+
97
+ Stessi filtri della lista. La colonna `message` segue la stessa logica di fallback.
98
+
99
+ ---
100
+
101
+ ## Modello dati Activity
102
+
103
+ Ogni activity segue il pattern **actor → verb → target** (Activity Streams).
104
+
105
+ ```ts
106
+ interface Activity {
107
+ _id: string;
108
+ id_project: string;
109
+ createdAt: string;
110
+ updatedAt: string;
111
+
112
+ /** Chi ha eseguito l'azione */
113
+ actor: {
114
+ type: 'user' | 'system';
115
+ id: string;
116
+ name?: string;
117
+ };
118
+
119
+ /** Tipo di evento — chiave per i18n */
120
+ verb: ActivityVerb;
121
+
122
+ /** Dettagli specifici dell'azione (varia per verb) */
123
+ actionObj?: Record<string, unknown>;
124
+
125
+ /** Oggetto su cui è stata eseguita l'azione */
126
+ target: {
127
+ type: string;
128
+ id: string;
129
+ object: Record<string, unknown>;
130
+ };
131
+
132
+ /** Fallback inglese (solo per alcuni verb, vedi sotto) */
133
+ message?: string;
134
+ }
135
+ ```
136
+
137
+ ### Ruoli dei campi
138
+
139
+ | Campo | Significato |
140
+ |---|---|
141
+ | `actor` | Chi ha **fatto** l'azione (utente loggato, o `system` per azioni automatiche) |
142
+ | `verb` | **Cosa** è successo — usare come chiave i18n principale |
143
+ | `target` | **Su cosa** — conversazione, project user, invito, ecc. |
144
+ | `actionObj` | Metadati aggiuntivi per costruire la frase (assignee, source, role, …) |
145
+
146
+ ---
147
+
148
+ ## Catalogo verb
149
+
150
+ ### Conversazioni — legacy
151
+
152
+ | Verb | Quando viene emesso |
153
+ |---|---|
154
+ | `REQUEST_CREATE` | Nuova conversazione creata dal visitatore |
155
+ | `REQUEST_CLOSE` | Conversazione chiusa da un agente |
156
+
157
+ ### Conversazioni — assegnazione (nuovi)
158
+
159
+ | Verb | Significato | Esempio frase (IT) |
160
+ |---|---|---|
161
+ | `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* |
162
+ | `REQUEST_ASSIGNED_SELF` | Un agente ha fatto **join/pick** manuale sulla conversazione | *L'utente **X** ha fatto join sulla conversazione **Y*** |
163
+ | `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`* |
164
+ | `REQUEST_UNASSIGNED` | Un agente è stato rimosso dalla conversazione | *L'utente **X** è stato rimosso dalla conversazione **Y*** |
165
+
166
+ ### Team / progetto — legacy
167
+
168
+ | Verb | Quando viene emesso |
169
+ |---|---|
170
+ | `PROJECT_USER_INVITE` | Invito di un utente al progetto |
171
+ | `PROJECT_USER_UPDATE` | Modifica ruolo, impostazioni o disponibilità di un altro utente (admin → agente) |
172
+ | `PROJECT_USER_DELETE` | Rimozione di un utente dal progetto |
173
+
174
+ ### Team — disponibilità (nuovi)
175
+
176
+ | Verb | Significato | Esempio frase (IT) |
177
+ |---|---|---|
178
+ | `PROJECT_USER_AVAILABILITY_SELF` | L'agente ha cambiato **il proprio** stato di disponibilità | *L'utente **X** ha modificato il suo stato in **inattivo*** |
179
+ | `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* |
180
+
181
+ > `PROJECT_USER_UPDATE` resta in uso per le modifiche fatte da un admin sul profilo di un altro utente (ruolo, disponibilità altrui, impostazioni, ecc.).
182
+
183
+ ### Chatbot (FAQ_KB)
184
+
185
+ | Verb | Significato | Esempio frase (IT) |
186
+ |---|---|---|
187
+ | `FAQ_KB_CREATE` | Creazione di un chatbot | *L'utente **X** ha creato il chatbot **Y*** |
188
+ | `FAQ_KB_DELETE` | Eliminazione di un chatbot | *L'utente **X** ha eliminato il chatbot **Y*** |
189
+ | `FAQ_KB_PUBLISH` | Pubblicazione di un chatbot | *L'utente **X** ha pubblicato il chatbot **Y*** |
190
+
191
+ > **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`.
192
+
193
+ `target.type` = `faq_kb` — nome in `target.object.name` o `actionObj.name`.
194
+
195
+ ### Knowledge Base — namespace e contenuti
196
+
197
+ | Verb | Significato | Esempio frase (IT) |
198
+ |---|---|---|
199
+ | `KB_NAMESPACE_CREATE` | Creazione di un nuovo namespace | *L'utente **X** ha creato il namespace **Y*** |
200
+ | `KB_NAMESPACE_DELETE` | Eliminazione completa di un namespace (namespace + contenuti) | *L'utente **X** ha eliminato il namespace **Y*** |
201
+ | `KB_CONTENTS_ADD` | Aggiunta di contenuti a un namespace | *L'utente **X** ha aggiunto … al namespace **Y*** |
202
+ | `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*** |
203
+
204
+ `target.type` = `kb_namespace` — nome in `target.object.name` o `actionObj.namespaceName`.
205
+
206
+ Per `KB_CONTENTS_ADD`, il tipo di aggiunta è in `actionObj.contentAddType`:
207
+
208
+ | `contentAddType` | Endpoint API | Descrizione |
209
+ |---|---|---|
210
+ | `content` | `POST /kb/` | Singolo contenuto (testo, URL, ecc.) |
211
+ | `url_list` | `POST /kb/multi` | Lista di URL |
212
+ | `csv` | `POST /kb/csv` | Import da file CSV |
213
+ | `sitemap` | `POST /kb/sitemap/import` | Import da sitemap |
214
+
215
+ ---
216
+
217
+ ## `actionObj` per i verb di disponibilità
218
+
219
+ Per `PROJECT_USER_AVAILABILITY_SELF` e `PROJECT_USER_AVAILABILITY_SYSTEM`:
220
+
221
+ ```ts
222
+ interface AvailabilityActionObj {
223
+ /** Valore inviato nel body della PUT */
224
+ user_available?: boolean;
225
+ profileStatus?: string;
226
+
227
+ /** Etichetta normalizzata del nuovo stato — usare per i18n */
228
+ newStatus?: string; // es. "available", "unavailable", "inactive"
229
+
230
+ /** Tipo semantico (ridondante con verb) */
231
+ updateType: 'self' | 'system' | 'admin';
232
+ source: 'api' | 'subscription' | 'system';
233
+ }
234
+ ```
235
+
236
+ > **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`).
237
+
238
+ ### Come distinguere self vs system
239
+
240
+ | Campo | Self | System |
241
+ |---|---|---|
242
+ | `verb` | `PROJECT_USER_AVAILABILITY_SELF` | `PROJECT_USER_AVAILABILITY_SYSTEM` |
243
+ | `actor.type` | `user` | `system` |
244
+ | `actor.id` | `id_user` di chi ha cambiato stato | `system` |
245
+ | `target.id` | `project_user_id` dell'agente | `project_user_id` dell'agente |
246
+ | `actionObj.updateType` | `self` | `system` |
247
+ | `actionObj.source` | `api` | `subscription` |
248
+
249
+ **Logica server (dopo la correzione):**
250
+
251
+ 1. **System** se `req.user` è una **Subscription** (JWT `sub: subscription`, oppure documento con `event` + `target` + `id_project`)
252
+ 2. **System** se il body contiene `"availabilityInitiator": "system"` (vedi sotto)
253
+ 3. **Self** se l'utente chiama `PUT /project_users/` (aggiornamento del proprio profilo)
254
+ 4. **Self** se `actor.id` coincide con `target.object.id_user._id` (confronto normalizzato)
255
+ 5. **Safety net in archiver:** se `verb` è `SELF` ma `actor.id ≠ target.id_user` → riclassificato come `PROJECT_USER_UPDATE` o `SYSTEM` (se subscription)
256
+
257
+ > **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.
258
+
259
+ ### Disconnessione da dashboard (subscription client)
260
+
261
+ Se il client di disconnessione usa il **token utente** (non JWT subscription) su `PUT /project_users/`, passare esplicitamente:
262
+
263
+ ```json
264
+ {
265
+ "user_available": false,
266
+ "profileStatus": "inactive",
267
+ "availabilityInitiator": "system"
268
+ }
269
+ ```
270
+
271
+ In alternativa usare JWT **subscription** su `PUT /project_users/:project_userid` — viene classificato automaticamente come system.
272
+
273
+ > **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.
274
+
275
+ ### Template frasi — disponibilità
276
+
277
+ **`PROJECT_USER_AVAILABILITY_SELF`**
278
+
279
+ | Lingua | Template |
280
+ |---|---|
281
+ | **IT** | `{{targetUser}} ha modificato il suo stato in {{newStatus}}` |
282
+ | **EN** | `{{targetUser}} changed availability status to {{newStatus}}` |
283
+
284
+ **`PROJECT_USER_AVAILABILITY_SYSTEM`**
285
+
286
+ | Lingua | Template |
287
+ |---|---|
288
+ | **IT** | `Lo stato di {{targetUser}} è stato modificato in {{newStatus}} dal sistema` |
289
+ | **EN** | `{{targetUser}} availability status was changed to {{newStatus}} by the system` |
290
+
291
+ `targetUser` si ricava da `target.object.id_user` (firstname + lastname).
292
+
293
+ ### Valori di `newStatus`
294
+
295
+ | Valore | Significato |
296
+ |---|---|
297
+ | `available` | `user_available: true` |
298
+ | `unavailable` | `user_available: false` |
299
+ | valore di `profileStatus` | es. `inactive`, `away`, … (se presente ha priorità su `user_available` nel label server) |
300
+
301
+ ---
302
+
303
+ ## `actionObj` per chatbot e Knowledge Base
304
+
305
+ ### Chatbot (`FAQ_KB_*`)
306
+
307
+ ```ts
308
+ interface FaqKbActionObj {
309
+ name?: string;
310
+ type?: string;
311
+ subtype?: string;
312
+ publishedBotId?: string; // solo FAQ_KB_PUBLISH
313
+ release_note?: string; // solo FAQ_KB_PUBLISH
314
+ }
315
+ ```
316
+
317
+ ### Namespace e contenuti (`KB_*`)
318
+
319
+ ```ts
320
+ interface KbNamespaceActionObj {
321
+ namespaceName?: string;
322
+ hybrid?: boolean;
323
+ default?: boolean; // KB_NAMESPACE_CREATE
324
+ deletedCount?: number; // KB_NAMESPACE_DELETE, KB_CONTENTS_DELETE
325
+ }
326
+
327
+ interface KbContentsAddActionObj {
328
+ contentAddType: 'content' | 'url_list' | 'csv' | 'sitemap';
329
+ namespaceName?: string;
330
+ count?: number;
331
+ type?: string; // tipo KB (txt, url, sitemap, …) — solo content/sitemap
332
+ source?: string; // URL o nome sorgente
333
+ }
334
+
335
+ interface KbContentsDeleteActionObj {
336
+ namespaceName?: string;
337
+ deletedCount?: number;
338
+ deleteMode: 'contents_only';
339
+ }
340
+ ```
341
+
342
+ ### Template frasi — chatbot e KB
343
+
344
+ | Verb | IT | EN |
345
+ |---|---|---|
346
+ | `FAQ_KB_CREATE` | `{{actor}} ha creato il chatbot {{chatbot}}` | `{{actor}} created chatbot {{chatbot}}` |
347
+ | `FAQ_KB_DELETE` | `{{actor}} ha eliminato il chatbot {{chatbot}}` | `{{actor}} deleted chatbot {{chatbot}}` |
348
+ | `FAQ_KB_PUBLISH` | `{{actor}} ha pubblicato il chatbot {{chatbot}}` | `{{actor}} published chatbot {{chatbot}}` |
349
+ | `KB_NAMESPACE_CREATE` | `{{actor}} ha creato il namespace {{namespace}}` | `{{actor}} created namespace {{namespace}}` |
350
+ | `KB_NAMESPACE_DELETE` | `{{actor}} ha eliminato il namespace {{namespace}}` | `{{actor}} deleted namespace {{namespace}}` |
351
+ | `KB_CONTENTS_ADD` (singolo) | `{{actor}} ha aggiunto un contenuto al namespace {{namespace}}` | `{{actor}} added content to namespace {{namespace}}` |
352
+ | `KB_CONTENTS_ADD` (multi) | `{{actor}} ha aggiunto {{count}} elementi ({{addType}}) al namespace {{namespace}}` | `{{actor}} added {{count}} items ({{addType}}) to namespace {{namespace}}` |
353
+ | `KB_CONTENTS_DELETE` | `{{actor}} ha eliminato tutti i contenuti dal namespace {{namespace}}` | `{{actor}} deleted all contents from namespace {{namespace}}` |
354
+
355
+ Per `KB_CONTENTS_ADD` con `contentAddType: 'sitemap'`, includere opzionalmente `{{source}}` (URL della sitemap).
356
+
357
+ ---
358
+
359
+ ## `actionObj` per i verb di assegnazione
360
+
361
+ Per i verb `REQUEST_ASSIGNED_*` e `REQUEST_UNASSIGNED`, `actionObj` ha questa forma:
362
+
363
+ ```ts
364
+ interface AssignmentActionObj {
365
+ /** id dell'assegnatario: id_user (agente), id chatbot o id dipartimento */
366
+ assigneeId: string;
367
+
368
+ /** Nome leggibile dell'assegnatario (agente, chatbot o dipartimento) */
369
+ assigneeName?: string;
370
+
371
+ /** Tipo di assegnatario */
372
+ assigneeType?: 'user' | 'bot' | 'department';
373
+
374
+ /** Tipo semantico (ridondante con verb, utile per debug) */
375
+ assignmentType: 'auto' | 'self_join' | 'manual_reassign' | 'manual_reassign_bot' | 'manual_reassign_department' | 'unassign';
376
+
377
+ /** Origine tecnica dell'evento */
378
+ source: 'api' | 'subscription' | 'chatbot' | 'queue' | 'webhook' | 'rules' | 'create' | 'system';
379
+
380
+ /** id_user dell'agente precedente (solo su reassign) */
381
+ previousAssigneeId?: string | null;
382
+
383
+ /** Partecipanti rimossi (solo su reassign/unassign) */
384
+ removedParticipants?: string[];
385
+ }
386
+ ```
387
+
388
+ ### Valori di `source`
389
+
390
+ | Source | Descrizione |
391
+ |---|---|
392
+ | `api` | Chiamata API dalla dashboard (utente reale) |
393
+ | `subscription` | Client automatico con JWT subscription (disconnessione, cleanup partecipanti, …) |
394
+ | `chatbot` | Handover dal chatbot (`PUT /agent`) |
395
+ | `queue` | Smart assignment / reroute automatico in coda |
396
+ | `webhook` | Sincronizzazione Chat21 (`join-member`) |
397
+ | `rules` | Trigger / regole automatiche |
398
+ | `create` | Assegnazione alla creazione della conversazione |
399
+ | `system` | Altri processi interni |
400
+
401
+ ### `actor` vs `assigneeId`
402
+
403
+ | Scenario | `actor` | `assigneeId` |
404
+ |---|---|---|
405
+ | Join manuale | L'agente che fa join (`actor.id === assigneeId`) | Stesso agente |
406
+ | Assegnazione automatica | `system` oppure l'utente che ha **scatenato** il routing | L'agente scelto dal round robin |
407
+ | Reassign manuale | Il supervisore/agente che riassegna | Il nuovo agente assegnato |
408
+ | Unassign | Chi ha rimosso il partecipante | L'agente rimosso |
409
+
410
+ > **Importante:** `assigneeId` è sempre un **`id_user`** (ID utente Tiledesk), non un `project_user._id`.
411
+
412
+ ---
413
+
414
+ ## Come costruire le frasi (i18n)
415
+
416
+ ### Strategia consigliata
417
+
418
+ ```
419
+ 1. Leggi activity.verb
420
+ 2. Risolvi i nomi da actor, actionObj, target
421
+ 3. Applica il template i18n corrispondente
422
+ 4. Se il template non esiste → usa activity.message (fallback inglese)
423
+ ```
424
+
425
+ ### Risoluzione nomi
426
+
427
+ ```ts
428
+ function actorName(activity: Activity): string {
429
+ if (activity.actor?.type === 'system') return t('ACTIVITY.SYSTEM'); // es. "Sistema"
430
+ return activity.actor?.name || activity.actor?.id || t('ACTIVITY.SOMEONE');
431
+ }
432
+
433
+ function conversationLabel(activity: Activity): string {
434
+ const request = activity.target?.object;
435
+ // Preferire un titolo leggibile se disponibile in futuro; oggi:
436
+ return request?.request_id || activity.target?.id || t('ACTIVITY.CONVERSATION');
437
+ }
438
+
439
+ function resolveAgentName(activity: Activity, userId?: string | null): string {
440
+ if (!userId) return t('ACTIVITY.UNKNOWN_AGENT');
441
+
442
+ const agents = activity.target?.object?.participatingAgents;
443
+ if (Array.isArray(agents)) {
444
+ for (const agent of agents) {
445
+ const user = agent.id_user || agent;
446
+ const id = String(user._id || user.id || user);
447
+ if (id === String(userId)) {
448
+ const name = [user.firstname, user.lastname].filter(Boolean).join(' ').trim();
449
+ if (name) return name;
450
+ }
451
+ }
452
+ }
453
+
454
+ // Fallback: cercare nel team del progetto se già in cache
455
+ return userId;
456
+ }
457
+ ```
458
+
459
+ ---
460
+
461
+ ## Template frasi — assegnazione conversazioni
462
+
463
+ ### `REQUEST_ASSIGNED_SELF`
464
+
465
+ L'utente ha fatto join sulla conversazione.
466
+
467
+ | Lingua | Template |
468
+ |---|---|
469
+ | **IT** | `{{actor}} ha fatto join sulla conversazione {{conversation}}` |
470
+ | **EN** | `{{actor}} joined conversation {{conversation}}` |
471
+
472
+ ```ts
473
+ // actor === assignee
474
+ const actor = actorName(activity);
475
+ const conversation = conversationLabel(activity);
476
+ // IT: "Mario Rossi ha fatto join sulla conversazione support-group-abc"
477
+ ```
478
+
479
+ ---
480
+
481
+ ### `REQUEST_ASSIGNED_AUTO`
482
+
483
+ La conversazione è stata assegnata automaticamente dal sistema.
484
+
485
+ Due varianti in base a `actor.type`:
486
+
487
+ | Condizione | IT | EN |
488
+ |---|---|---|
489
+ | `actor.type === 'system'` | `All'utente **{{assignee}}** è stata assegnata la conversazione **{{conversation}}** dal sistema` | `Conversation **{{conversation}}** was automatically assigned to **{{assignee}}** by the system` |
490
+ | `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}}**)` |
491
+
492
+ ```ts
493
+ const assignee = resolveAgentName(activity, activity.actionObj?.assigneeId);
494
+ const conversation = conversationLabel(activity);
495
+ const actor = actorName(activity);
496
+
497
+ if (activity.actor?.type === 'system') {
498
+ // "All'utente Mario Rossi è stata assegnata la conversazione support-group-abc dal sistema"
499
+ } else {
500
+ // "All'utente Mario Rossi è stata assegnata la conversazione support-group-abc (assegnazione automatica avviata da Laura Bianchi)"
501
+ }
502
+ ```
503
+
504
+ ---
505
+
506
+ ### `REQUEST_ASSIGNED_MANUAL`
507
+
508
+ Un operatore ha assegnato la conversazione a un altro agente, a un chatbot o a un dipartimento.
509
+
510
+ Usare `actionObj.assigneeType` per distinguere i tre casi. Se presente, preferire `actionObj.assigneeName` al posto della risoluzione da `assigneeId`.
511
+
512
+ | `assigneeType` | IT | EN |
513
+ |---|---|---|
514
+ | `user` (default) | `All'utente **{{assignee}}** è stata assegnata la conversazione **{{conversation}}** da **{{actor}}**` | `**{{actor}}** assigned conversation **{{conversation}}** to **{{assignee}}**` |
515
+ | `bot` | `**{{actor}}** ha riassegnato la conversazione **{{conversation}}** al chatbot **{{assignee}}**` | `**{{actor}}** reassigned conversation **{{conversation}}** to chatbot **{{assignee}}**` |
516
+ | `department` | `**{{actor}}** ha riassegnato la conversazione **{{conversation}}** al dipartimento **{{assignee}}**` | `**{{actor}}** reassigned conversation **{{conversation}}** to department **{{assignee}}**` |
517
+
518
+ **Con agente precedente** (`previousAssigneeId` presente, tipico su reassign):
519
+
520
+ | `assigneeType` | IT | EN |
521
+ |---|---|---|
522
+ | `user` | `All'utente **{{assignee}}** è stata assegnata la conversazione **{{conversation}}** da **{{actor}}** (sostituisce **{{previous}}**)` | `**{{actor}}** reassigned conversation **{{conversation}}** to **{{assignee}}** (replacing **{{previous}}**)` |
523
+ | `bot` / `department` | Aggiungere `(sostituisce **{{previous}}**)` / `(replacing **{{previous}}**)` | stesso pattern |
524
+
525
+ ```ts
526
+ const actor = actorName(activity);
527
+ const assignee = activity.actionObj?.assigneeName || resolveAssigneeName(activity);
528
+ const assigneeType = activity.actionObj?.assigneeType || 'user';
529
+ const previous = resolveAgentName(activity, activity.actionObj?.previousAssigneeId);
530
+ const conversation = conversationLabel(activity);
531
+ // IT bot: "Laura Bianchi ha riassegnato la conversazione support-group-abc al chatbot Support Bot"
532
+ // IT department: "Laura Bianchi ha riassegnato la conversazione support-group-abc al dipartimento Vendite"
533
+ ```
534
+
535
+ ---
536
+
537
+ ### `REQUEST_UNASSIGNED`
538
+
539
+ | Lingua | Template |
540
+ |---|---|
541
+ | **IT** | `**{{actor}}** ha rimosso **{{assignee}}** dalla conversazione **{{conversation}}**` |
542
+ | **EN** | `**{{actor}}** unassigned **{{assignee}}** from conversation **{{conversation}}**` |
543
+
544
+ **Via subscription** (`actionObj.source === 'subscription'`, tipico disconnessione agente):
545
+
546
+ | Lingua | Template |
547
+ |---|---|
548
+ | **IT** | `**{{assignee}}** è stato rimosso dalla conversazione **{{conversation}}** dal sistema` |
549
+ | **EN** | `**{{assignee}}** was unassigned from conversation **{{conversation}}** by the system` |
550
+
551
+ In questo caso `actor.type` = `system`, `actor.id` = `system` (non l'id della subscription).
552
+
553
+ > **Record legacy:** se `actor.type === 'user'` ma `actor.id` è un id subscription, trattare come `source: subscription` e actor = System.
554
+
555
+ ---
556
+
557
+ ## Template frasi — conversazioni legacy
558
+
559
+ ### `REQUEST_CREATE`
560
+
561
+ | Lingua | Template |
562
+ |---|---|
563
+ | **IT** | `**{{actor}}** ha avviato una nuova conversazione` |
564
+ | **EN** | `**{{actor}}** started a new conversation` |
565
+
566
+ `actor` = visitatore (`requester_id` / `requester_name`).
567
+ `target.object` contiene la request completa (`first_text`, `request_id`, …).
568
+
569
+ ### `REQUEST_CLOSE`
570
+
571
+ | Lingua | Template |
572
+ |---|---|
573
+ | **IT** | `**{{actor}}** ha chiuso la conversazione **{{conversation}}**` |
574
+ | **EN** | `**{{actor}}** closed conversation **{{conversation}}**` |
575
+
576
+ `actor.id` = `closed_by`, `actor.name` = `closed_by_name`.
577
+
578
+ ---
579
+
580
+ ## Template frasi — team legacy
581
+
582
+ ### `PROJECT_USER_INVITE`
583
+
584
+ Invito di un teammate al progetto (`POST /project_users/invite`). Copre sia utenti già registrati sia inviti in pending (email non ancora su Tiledesk).
585
+
586
+ | Lingua | Template |
587
+ |---|---|
588
+ | **IT** | `**{{actor}}** ha invitato **{{target}}** ({{email}}) ad assumere il ruolo di **{{role}}**` |
589
+ | **EN** | `**{{actor}}** invited **{{target}}** ({{email}}) to take on the role of **{{role}}**` |
590
+
591
+ Dati da usare:
592
+ - `actor.name` (da `activityActorUtil` / `req.user`)
593
+ - `actionObj.email`, `actionObj.role`
594
+ - `actionObj.inviteType`: `'registered'` (utente esistente) | `'pending'` (pending invitation)
595
+ - `target.type`: `pendinginvitation` vs `project_user`
596
+ - `target.object.id_user.firstname/lastname` per utenti registrati, oppure `target.object.email` per pending
597
+
598
+ ```ts
599
+ const actor = actorName(activity);
600
+ const target = inviteTargetLabel(activity); // nome o email
601
+ const email = activity.actionObj?.email || target;
602
+ const role = activity.actionObj?.role || 'agent';
603
+ ```
604
+
605
+ ### `PROJECT_USER_DELETE`
606
+
607
+ Rimozione di un teammate dal progetto (`DELETE /project_users/:id?soft=true` o `?hard=true`).
608
+
609
+ | Lingua | Template |
610
+ |---|---|
611
+ | **IT** | `**{{actor}}** ha rimosso **{{target}}** dal progetto` |
612
+ | **EN** | `**{{actor}}** removed **{{target}}** from the project` |
613
+
614
+ Dati da usare:
615
+ - `actor.name`
616
+ - `target.object.id_user.firstname/lastname` (o `actionObj.email` come fallback)
617
+ - `actionObj.deleteType`: `'soft'` | `'hard'`
618
+ - `actionObj.role`: ruolo dell'utente rimosso
619
+
620
+ ```ts
621
+ const actor = actorName(activity);
622
+ const target = targetUserLabel(activity);
623
+ // IT: "Laura Bianchi ha rimosso Mario Rossi dal progetto"
624
+ ```
625
+
626
+ ### `PROJECT_USER_UPDATE`
627
+
628
+ Due sotto-casi:
629
+
630
+ **A) L'utente modifica se stesso** (`actor.id === target.object.id_user._id`)
631
+
632
+ | Campo cambiato | IT | EN |
633
+ |---|---|---|
634
+ | `actionObj.user_available === true` | `{{actor}} ha cambiato il suo stato in disponibile` | `{{actor}} changed his status to available` |
635
+ | `actionObj.user_available === false` | `{{actor}} ha cambiato il suo stato in non disponibile` | `{{actor}} changed his status to unavailable` |
636
+
637
+ **B) Un admin modifica un altro utente**
638
+
639
+ | Campo cambiato | IT | EN |
640
+ |---|---|---|
641
+ | `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` |
642
+ | `actionObj.role === 'admin'` | `{{actor}} ha cambiato il ruolo di {{target}} in Amministratore` | `{{actor}} changed the role of {{target}} to Administrator` |
643
+ | `actionObj.role === 'agent'` | `{{actor}} ha cambiato il ruolo di {{target}} in Agente` | `{{actor}} changed the role of {{target}} to Agent` |
644
+
645
+ ---
646
+
647
+ ## Chiavi i18n suggerite
648
+
649
+ ```json
650
+ {
651
+ "ACTIVITY": {
652
+ "SYSTEM": "Sistema",
653
+ "SOMEONE": "Qualcuno",
654
+ "CONVERSATION": "conversazione",
655
+ "UNKNOWN_AGENT": "agente sconosciuto",
656
+
657
+ "REQUEST_ASSIGNED_SELF": "{{actor}} ha fatto join sulla conversazione {{conversation}}",
658
+ "REQUEST_ASSIGNED_AUTO_SYSTEM": "All'utente {{assignee}} è stata assegnata la conversazione {{conversation}} dal sistema",
659
+ "REQUEST_ASSIGNED_AUTO_TRIGGERED": "All'utente {{assignee}} è stata assegnata la conversazione {{conversation}} (assegnazione automatica avviata da {{actor}})",
660
+ "REQUEST_ASSIGNED_MANUAL": "All'utente {{assignee}} è stata assegnata la conversazione {{conversation}} da {{actor}}",
661
+ "REQUEST_ASSIGNED_MANUAL_REPLACED": "All'utente {{assignee}} è stata assegnata la conversazione {{conversation}} da {{actor}} (sostituisce {{previous}})",
662
+ "REQUEST_ASSIGNED_MANUAL_BOT": "{{actor}} ha riassegnato la conversazione {{conversation}} al chatbot {{assignee}}",
663
+ "REQUEST_ASSIGNED_MANUAL_BOT_REPLACED": "{{actor}} ha riassegnato la conversazione {{conversation}} al chatbot {{assignee}} (sostituisce {{previous}})",
664
+ "REQUEST_ASSIGNED_MANUAL_DEPARTMENT": "{{actor}} ha riassegnato la conversazione {{conversation}} al dipartimento {{assignee}}",
665
+ "REQUEST_ASSIGNED_MANUAL_DEPARTMENT_REPLACED": "{{actor}} ha riassegnato la conversazione {{conversation}} al dipartimento {{assignee}} (sostituisce {{previous}})",
666
+ "REQUEST_UNASSIGNED": "{{actor}} ha rimosso {{assignee}} dalla conversazione {{conversation}}",
667
+ "REQUEST_UNASSIGNED_SYSTEM": "{{assignee}} è stato rimosso dalla conversazione {{conversation}} dal sistema",
668
+
669
+ "PROJECT_USER_AVAILABILITY_SELF": "{{targetUser}} ha modificato il suo stato in {{newStatus}}",
670
+ "PROJECT_USER_AVAILABILITY_SYSTEM": "Lo stato di {{targetUser}} è stato modificato in {{newStatus}} dal sistema",
671
+
672
+ "FAQ_KB_CREATE": "{{actor}} ha creato il chatbot {{chatbot}}",
673
+ "FAQ_KB_DELETE": "{{actor}} ha eliminato il chatbot {{chatbot}}",
674
+ "FAQ_KB_PUBLISH": "{{actor}} ha pubblicato il chatbot {{chatbot}}",
675
+ "KB_NAMESPACE_CREATE": "{{actor}} ha creato il namespace {{namespace}}",
676
+ "KB_NAMESPACE_DELETE": "{{actor}} ha eliminato il namespace {{namespace}}",
677
+ "KB_CONTENTS_ADD": "{{actor}} ha aggiunto contenuti al namespace {{namespace}}",
678
+ "KB_CONTENTS_ADD_MULTI": "{{actor}} ha aggiunto {{count}} elementi ({{addType}}) al namespace {{namespace}}",
679
+ "KB_CONTENTS_DELETE": "{{actor}} ha eliminato tutti i contenuti dal namespace {{namespace}}",
680
+
681
+ "REQUEST_CREATE": "{{actor}} ha avviato una nuova conversazione",
682
+ "REQUEST_CLOSE": "{{actor}} ha chiuso la conversazione {{conversation}}",
683
+
684
+ "PROJECT_USER_INVITE": "{{actor}} ha invitato {{target}} ({{email}}) ad assumere il ruolo di {{role}}",
685
+ "PROJECT_USER_DELETE": "{{actor}} ha rimosso {{target}} dal progetto",
686
+ "PROJECT_USER_UPDATE_SELF_AVAILABLE": "{{actor}} ha cambiato il suo stato in disponibile",
687
+ "PROJECT_USER_UPDATE_SELF_UNAVAILABLE": "{{actor}} ha cambiato il suo stato in non disponibile",
688
+ "PROJECT_USER_UPDATE_AVAILABILITY": "{{actor}} ha cambiato lo stato di disponibilità di {{target}}",
689
+ "PROJECT_USER_UPDATE_ROLE_ADMIN": "{{actor}} ha cambiato il ruolo di {{target}} in Amministratore",
690
+ "PROJECT_USER_UPDATE_ROLE_AGENT": "{{actor}} ha cambiato il ruolo di {{target}} in Agente"
691
+ }
692
+ }
693
+ ```
694
+
695
+ ---
696
+
697
+ ## Funzione di rendering consigliata (pseudocodice)
698
+
699
+ ```ts
700
+ function renderActivity(activity: Activity, t: TranslateFn): string {
701
+ // 1. Fallback server
702
+ if (!hasTranslation(activity.verb)) {
703
+ return activity.message || activity.verb;
704
+ }
705
+
706
+ const actor = actorName(activity);
707
+ const conversation = conversationLabel(activity);
708
+ const assignee = resolveAgentName(activity, activity.actionObj?.assigneeId);
709
+ const previous = resolveAgentName(activity, activity.actionObj?.previousAssigneeId);
710
+
711
+ switch (activity.verb) {
712
+
713
+ case 'REQUEST_ASSIGNED_SELF':
714
+ return t('ACTIVITY.REQUEST_ASSIGNED_SELF', { actor, conversation });
715
+
716
+ case 'REQUEST_ASSIGNED_AUTO':
717
+ return activity.actor?.type === 'system'
718
+ ? t('ACTIVITY.REQUEST_ASSIGNED_AUTO_SYSTEM', { assignee, conversation })
719
+ : t('ACTIVITY.REQUEST_ASSIGNED_AUTO_TRIGGERED', { assignee, conversation, actor });
720
+
721
+ case 'REQUEST_ASSIGNED_MANUAL': {
722
+ const assigneeType = activity.actionObj?.assigneeType || 'user';
723
+ const resolvedAssignee = activity.actionObj?.assigneeName || assignee;
724
+ if (assigneeType === 'bot') {
725
+ return activity.actionObj?.previousAssigneeId
726
+ ? t('ACTIVITY.REQUEST_ASSIGNED_MANUAL_BOT_REPLACED', { assignee: resolvedAssignee, conversation, actor, previous })
727
+ : t('ACTIVITY.REQUEST_ASSIGNED_MANUAL_BOT', { assignee: resolvedAssignee, conversation, actor });
728
+ }
729
+ if (assigneeType === 'department') {
730
+ return activity.actionObj?.previousAssigneeId
731
+ ? t('ACTIVITY.REQUEST_ASSIGNED_MANUAL_DEPARTMENT_REPLACED', { assignee: resolvedAssignee, conversation, actor, previous })
732
+ : t('ACTIVITY.REQUEST_ASSIGNED_MANUAL_DEPARTMENT', { assignee: resolvedAssignee, conversation, actor });
733
+ }
734
+ return activity.actionObj?.previousAssigneeId
735
+ ? t('ACTIVITY.REQUEST_ASSIGNED_MANUAL_REPLACED', { assignee: resolvedAssignee, conversation, actor, previous })
736
+ : t('ACTIVITY.REQUEST_ASSIGNED_MANUAL', { assignee: resolvedAssignee, conversation, actor });
737
+ }
738
+
739
+ case 'REQUEST_UNASSIGNED':
740
+ return activity.actionObj?.source === 'subscription'
741
+ ? t('ACTIVITY.REQUEST_UNASSIGNED_SYSTEM', { assignee, conversation })
742
+ : t('ACTIVITY.REQUEST_UNASSIGNED', { actor, assignee, conversation });
743
+
744
+ case 'REQUEST_CREATE':
745
+ return t('ACTIVITY.REQUEST_CREATE', { actor });
746
+
747
+ case 'REQUEST_CLOSE':
748
+ return t('ACTIVITY.REQUEST_CLOSE', { actor, conversation });
749
+
750
+ // ... PROJECT_USER_* come da tabella sopra
751
+
752
+ default:
753
+ return activity.message || activity.verb;
754
+ }
755
+ }
756
+ ```
757
+
758
+ ---
759
+
760
+ ## Esempi completi
761
+
762
+ ### Join manuale
763
+
764
+ ```json
765
+ {
766
+ "verb": "REQUEST_ASSIGNED_SELF",
767
+ "actor": { "type": "user", "id": "user_abc", "name": "Mario Rossi" },
768
+ "actionObj": {
769
+ "assigneeId": "user_abc",
770
+ "assignmentType": "self_join",
771
+ "source": "api"
772
+ },
773
+ "target": {
774
+ "type": "request",
775
+ "object": { "request_id": "support-group-xyz" }
776
+ }
777
+ }
778
+ ```
779
+
780
+ → **IT:** `Mario Rossi ha fatto join sulla conversazione support-group-xyz`
781
+
782
+ ---
783
+
784
+ ### Assegnazione automatica (sistema)
785
+
786
+ ```json
787
+ {
788
+ "verb": "REQUEST_ASSIGNED_AUTO",
789
+ "actor": { "type": "system", "id": "system", "name": "System" },
790
+ "actionObj": {
791
+ "assigneeId": "user_abc",
792
+ "assignmentType": "auto",
793
+ "source": "queue"
794
+ },
795
+ "target": {
796
+ "type": "request",
797
+ "object": {
798
+ "request_id": "support-group-xyz",
799
+ "participatingAgents": [
800
+ { "id_user": { "_id": "user_abc", "firstname": "Mario", "lastname": "Rossi" } }
801
+ ]
802
+ }
803
+ }
804
+ }
805
+ ```
806
+
807
+ → **IT:** `All'utente Mario Rossi è stata assegnata la conversazione support-group-xyz dal sistema`
808
+
809
+ ---
810
+
811
+ ### Reassign manuale
812
+
813
+ ```json
814
+ {
815
+ "verb": "REQUEST_ASSIGNED_MANUAL",
816
+ "actor": { "type": "user", "id": "user_supervisor", "name": "Laura Bianchi" },
817
+ "actionObj": {
818
+ "assigneeId": "user_abc",
819
+ "assignmentType": "manual_reassign",
820
+ "source": "api",
821
+ "previousAssigneeId": "user_old"
822
+ },
823
+ "target": {
824
+ "type": "request",
825
+ "object": { "request_id": "support-group-xyz" }
826
+ }
827
+ }
828
+ ```
829
+
830
+ → **IT:** `All'utente Mario Rossi è stata assegnata la conversazione support-group-xyz da Laura Bianchi`
831
+
832
+ ---
833
+
834
+ ## Filtri dashboard consigliati
835
+
836
+ Per una vista "Assegnazioni conversazioni":
837
+
838
+ ```
839
+ GET /{project_id}/activities?activities=REQUEST_ASSIGNED_AUTO,REQUEST_ASSIGNED_SELF,REQUEST_ASSIGNED_MANUAL,REQUEST_UNASSIGNED
840
+ ```
841
+
842
+ Per storico completo includere anche `REQUEST_CREATE` e `REQUEST_CLOSE`.
843
+
844
+ Per attività chatbot e Knowledge Base:
845
+
846
+ ```
847
+ 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
848
+ ```
849
+
850
+ Per attività di un singolo agente:
851
+
852
+ ```
853
+ GET /{project_id}/activities?agent_id={user_id}
854
+ ```
855
+
856
+ ---
857
+
858
+ ## Note implementative
859
+
860
+ 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`).
861
+ 2. **Non usare l'endpoint HTTP** per capire il tipo di assegnazione — usare sempre `verb` e `actionObj.assignmentType`.
862
+ 3. **`actor.id` su `PROJECT_USER_UPDATE`** può essere `_id` MongoDB, mentre su altri eventi è `user.id` — confrontare sempre come stringa.
863
+ 4. **Preflight** — le request con `preflight: true` non generano activity di creazione.
864
+ 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.