Files
csbot-prototype/documentation/18-ticketovaci-system.md
T
JiriUhlirandClaude Opus 5 a771834e57 Realne sluzby, OpenAI, odesilani e-mailu a helpdesk
Katalog srovnany s tim, co opravdu bezi na services.csbot.cz/apps:
trinact sluzeb dostalo pristupove udaje a levne cteci overeni, opravena
appId, ktera nikam nevedla (ppl, microsoft365, transcription), a GA4,
Search Console, Google Ads i Sklik ted stoji na aplikaci analytics,
kazda s vlastnimi udaji. Nove sluzby SAP Business One, Google Workspace
a Meta Ads. K tomu 23 skriptu, ktere s nimi opravdu neco delaji.

OpenAI jako prvni sluzba, ktera nebezi u nas: Service.baseUrl s absolutni
adresou, prepis pres <SLUZBA>_BASE_URL nebo adresu u konektoru, predpona
hlavicky u pole udaju (uzivatel vlepi holy klic, Bearer dopise runtime).
Dotaz na model, nahrani souboru, otazka nad souborem, prepis zvuku.
Skript umi odeslat soubor pres ctx.http.postForm (multipart, obsah Base64).

Sluzba E-mail pres SMTP. Neni to skript, ale vnitrni krok - SMTP neni HTTP.
Konektor nese schranku firmy, krok ma HTML telo, ve kterem se dosazene
hodnoty escapuji (znacky autora sablony jsou zamer, ostre zavorky od
zakaznika ne). Overeni konektoru se prihlasi na server a nic neodesle.

Helpdesk: Ticket.helpdeskSourceId drzi firmu, ktera pozadavek poslala,
vlastnikem zustava ta, ktera ho resi - jinak by ho resitel nemel ve sve
fronte. Komu pozadavek pripadne, urcuje Tenant.helpdeskProviderId.
Zadavatel vidi jen svoje pozadavky a smi k nim pripsat komentar.

Opravy v portalu:
- hlasky o ulozisti a odchozi IP vidi jen spravce platformy
- typ ticketu se v automatizaci vybira ze seznamu firmy, nebo dosadi z dat
- stav ticketu je otevreny naseptavac, ne ciselnik
- ticket jde zalozit rucne, zakaznik u nej neni povinny
- kanal se prejmenoval a parametry u webhooku jsou oznacene jako nepovinne
- srovnane markdown tabulky v cele dokumentaci

Co z teto davky jeste neni: prepinac firmy je porad jen stav uvnitr stranky
Prehled, takze se prepnuti neprojevi v Lidech ani jinde.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 07:40:16 +02:00

11 KiB

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.

Ticketem se může stát jakákoliv událost

Vstup je jeden veřejný endpoint na firmu:

POST /webhook/ticket/<token>

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:

{
  "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/<token>  {"externalId": 3, "event": "order.created"}   -> 201, vznikl TK-4822
POST /webhook/ticket/<token>  {"externalId": 3, "event": "email.sent"}      -> 200, doplněn TK-4822
POST /webhook/ticket/<jiná firma>  {"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.

{
  "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.

Helpdesk: ticket, ktery vidi dve firmy

Ticket patri jedne firme. U helpdesku ale figuruji dve: ta, ktera pozadavek poslala, a ta, ktera ho resi. Reseni je jedno pole navic, ne druha hranice viditelnosti.

Pole Kdo to je
Ticket.tenantId firma, ktera pozadavek resi, tedy vlastnik
Ticket.helpdeskSourceId firma, ktera pozadavek poslala

Vlastnikem je zamerne dodavatel, ne zadavatel. Kdyby byl vlastnikem zadavatel, mel by resitel pozadavek jen jako cizi ticket a nemel by ho ve sve fronte, ve statistikach ani v prirazovani. Takhle je to na jeho strane obycejny ticket a nemuselo se kvuli tomu sahnout na nic z toho, co uz funguje.

Komu pozadavek pripadne, urcuje helpdeskProviderId na firme zadavatele. Nastavuje ho spravce platformy v Nastaveni, Firmy. Kdo koho obsluhuje je obchodni vztah, ne volba klienta - kdyby si dodavatele vybiral uzivatel, poslal by pozadavek nekomu, s kym nema smlouvu. Bez vyplneneho dodavatele se pozadavek nezalozi a rekne se to nahlas.

Co smi zadavatel

Akce Smi
Videt svoje pozadavky ano
Otevrit detail a prubeh ano
Pripsat komentar ano
Menit stav, resitele, typ ne
Videt ostatni tickety resitele ne

Komentar je jedina zmena, kterou nad cizim ticketem smi. Doplnit, co zapomnel napsat, je presne to, kvuli cemu se pozadavek otevira; stav urcuje ten, kdo to resi.

Filtruje se podle helpdeskSourceIds, ktere nahrazuje filtr podle vlastnika - zadavatel vlastnikem neni, takze by mu jinak nezbylo nic. Bezny seznam ticketu tim zustava nedotceny: listTickets({ tenantIds }) se nezmenil.

Kdo helpdesk vidi

Pravo helpdesk.view (videt sekci) a helpdesk.create (poslat pozadavek). Obe prideluje admin te firmy pres role, stejne jako u ostatnich prav. Zalozka helpdesk je v katalogu modulu jako povinna, aby ji mely i firmy zalozene driv - o tom, kdo ji uvidi, stejne rozhoduje pravo.

API

Metoda Cesta Popis
GET /api/dashboard/helpdesk pozadavky teto firmy
POST /api/dashboard/helpdesk poslat pozadavek
GET /api/dashboard/helpdesk/:id detail vlastniho pozadavku
POST /api/dashboard/helpdesk/:id/comment pripsat komentar

Stranka portalu je /dashboard/helpdesk.

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.

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.