# Ticketovací systém Jak se z události stane ticket, jak se k němu navěsí další, a co se z toho dá vyčíst. Model práv a typů je v [17-nastaveni-a-prava.md](17-nastaveni-a-prava.md). ## Ticketem se může stát jakákoliv událost Vstup je jeden veřejný endpoint na firmu: ``` POST /webhook/ticket/ ``` Token patří **firmě**, ne automatizaci. Založit ticket má jít i bez toho, aby se kvůli tomu stavěl strom. Adresu najde ten, kdo spravuje napojení, na `GET /api/dashboard/intake`; přegeneruje se `POST /api/dashboard/intake/regenerate` a stará okamžitě přestane platit. Tělo: ```json { "externalId": 3, "source": "eshop", "event": "order.created", "subject": "Objednávka 3", "typeId": "tt_order", "fields": { "orderNumber": "3", "orderTotal": 2450 }, "tags": ["vip"], "payload": { "cokoliv": "co se má uložit celé" } } ``` Povinné není nic kromě těla samotného. Chybí předmět? Použije se popisek nebo typ události. Neznámý `typeId`? Zahodí se a zaloguje, ticket vznikne bez typu - odmítnout celou událost kvůli překlepu v jednom poli by znamenalo ztrátu dat. ## Externí ID a navěšování `externalId` je ID u odesílatele, typicky číslo objednávky. Je **unikátní v rámci firmy**, ne globálně: dvě firmy můžou mít objednávku číslo 3 a nesmí si o sebe zavadit. Firmu určuje token v adrese. Když už ticket se stejným externím ID v té firmě je, událost se na něj **navěsí** místo založení druhého: ``` POST /webhook/ticket/ {"externalId": 3, "event": "order.created"} -> 201, vznikl TK-4822 POST /webhook/ticket/ {"externalId": 3, "event": "email.sent"} -> 200, doplněn TK-4822 POST /webhook/ticket/ {"externalId": 3} -> 201, vznikl TK-4823 ``` Číslo a řetězec jsou tentýž klíč: odesílatel pošle `3`, my držíme `"3"`. Jinak by `3` a `"3"` byly dva tickety a nikdo by nepoznal proč. Bez `externalId` se vždycky zakládá nový ticket. Hádat podle předmětu by slučovalo věci, které spolu nesouvisí. ### Událost není řádek logu Dvě různé věci, které se snadno pletou: | | Co to je | Kde se bere | | --- | --- | --- | | **Událost** | fakt zvenku, celá přijatá data | poslal odesílatel | | **Řádek logu** | naše stopa toho, co se dělo uvnitř | zapsala aplikace | Události se ukazují na detailu ticketu nad logem a dají se rozbalit na celý přijatý JSON. Když se někdo ptá, proč ticket vypadá takhle, je to jediná odpověď. Drží se jich nejvýš 200 na ticket, starší se odmazávají - nekonečně rostoucí ticket by při každém zápisu přepisoval víc a víc dat. ## Co se u ticketu měří Aby šlo říct, kdo kolik odbavil a komu to nejde, nestačí počítat vyřešené. Ticket proto nese: | Pole | Kdy se zapíše | K čemu | | --- | --- | --- | | `firstResponseAt` | při prvním přiřazení, komentáři nebo změně stavu | jak dlouho zákazník čekal na reakci | | `resolvedAt` | při přechodu na vyřešeno | doba řešení | | `resolvedById` | tamtéž, je to ten, kdo ho měl u sebe | komu se vyřešení připíše | | `reopenCount` | při návratu z vyřešeno | kolikrát to hotové nebylo | `firstResponseAt` se zapisuje **jednou a nepřepisuje**. Je to okamžik, kdy zákazník přestal čekat. Kdyby se přepisoval při každé změně, měřil by poslední dotek, což je úplně jiná veličina. `reopenCount` je záměrně vedle počtu vyřešených. Samotný počet vyřešených odměňuje toho, kdo tickety zavíral předčasně. ## Výkon řešitelů Widget **Výkon řešitelů** (`panel.agents`) a detail osoby ukazují za 30 dní: - **odbaveno** - kolik vyřešil, - **ve frontě** - kolik má právě teď, - **doba řešení** a **reakce** - mediány, - **vráceno** - kolik se mu jich vrátilo, - **nejstarší** - co mu leží nejdéle. Medián, ne průměr: jeden ticket zapomenutý přes dovolenou by průměr úplně rozhodil. Fronta se počítá vždycky celá, bez ohledu na období - leží tam bez ohledu na to, na co se zrovna díváme. Čísla počítá `getAgentStats` v `src/data/ticketStore.ts` a používá je widget i detail osoby. Kdyby si je stránka počítala sama, na dvou místech by vyšlo něco jiného. ## Pohledy | Stránka | Co ukazuje | | --- | --- | | Tickety | seznam s filtry, **tabulka nebo dlaždice** | | Detail ticketu | obsah, události, log, akce, typ, tagy, řešitel, skupina | | Lidé | řešitelé firmy a jejich vytížení, **tabulka nebo dlaždice** | | Detail osoby | její výkon, co má u sebe, co naposledy vyřešila | Přepínač pohledu je jedna komponenta (`components/dashboard/ViewSwitch.tsx`) a používají ji obě stránky se seznamem. ## Widgety nad daty i nad konektory Widget je dvojice: `render` (jak se to kreslí) a `source` (odkud jsou data). Zdroje: | Zdroj | Co dělá | | --- | --- | | `ticketCount` | počet ticketů podle filtru, volitelně seskupený | | `ticketList` | seznam ticketů | | `ticketSeries` | časová řada | | `workload` | kdo co má u sebe | | `agentStats` | výkon řešitelů | | `connector` | **data z napojené služby** | Zdroj `connector` zavolá **tentýž skript**, který používá krok automatizace i akce na ticketu, a z výsledku vezme, co je v `path`. Widget nemá vlastní cestu k cizí službě - jinak by se chovala jinak než zbytek aplikace. ```json { "kind": "connector", "serviceId": "idoklad", "operationId": "find-issued-invoice", "connectorId": null, "inputs": {}, "path": "total", "ttlSec": 300 } ``` Výsledek se **drží v mezipaměti** (`ttlSec`, nejméně 30 s). Bez toho by každé otevření přehledu znamenalo volání cizího API za každou dlaždici, a to má limity a někdy se za něj platí. U dat z konektoru se vedle čísla ukazuje jejich stáří a jestli jsou z mezipaměti. Data všech dlaždic chodí **jedním requestem** (`POST /api/dashboard/widget-data`). Widget, který selže, hlásí chybu **na své pozici** a celou, nezkrácenou - jeden rozbitý zdroj nesmí zhasnout celý přehled. ## Kde se co definuje Akce a widgety **nejsou v nastavení**. Je to definice toho, co aplikace umí, stejná úroveň jako automatizace, a mají vlastní záložku: | Záložka | Co tam patří | | --- | --- | | Automatizace | stromy, které běží samy | | Akce | tlačítka na ticketu, tělo je **tentýž strom** | | Widgety | dlaždice na přehled | | Nastavení | firmy, lidé, role, typy ticketů, audit | Tělo akce se skládá stejným editorem jako automatizace. Místo karty spouštěče je karta "spouští člověk tlačítkem na ticketu" a parametry, na které jde v krocích odkazovat, jsou údaje ticketu plus vlastní pole jeho typu. Ty počítá server (`GET /api/dashboard/settings/actions/:id/scope`), aby si klient nedělal druhý seznam, který se časem rozejde. ## Co zatím nejde Uložený strom se **nevykoná** - chybí runtime, stejně jako u automatizací. Akce s jednou operací běží, protože ta se dá poslat rovnou do skriptu. Popis toho, jak má runtime vypadat, je v [10-runtime-a-kapacita.md](10-runtime-a-kapacita.md). Externí ID hlídá jedinečnost v paměti procesu. Nad databází k tomu patří částečný unikátní index na dvojici `(tenant_id, external_id)`, aby to platilo i při běhu na víc strojích.