UNPKG

@tiledesk/tiledesk-server

Version:
865 lines (648 loc) 34.2 kB
# 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.