# 06 - Tickety ## Co ticket je a co neni Ticket je **prichozi pozadavek odkudkoliv**. Prisla zprava na WhatsApp, Messenger nebo Instagram, prisel e-mail, nekdo zavolal na hlasovou linku, odeslal formular. Z toho vznikne ticket, ktery ma sveho cloveka a dohledatelny prubeh. Ticket **neni** bug ani wish. Vyvojarska agenda je jina vec s jinym zivotnim cyklem a v prototypu zatim neexistuje. Michat je do jedne evidence by znamenalo, ze ani jedna nefunguje poradne. ## Tri veci, na kterych to stoji **Kanaly ustuji do ticketu.** WhatsApp, e-mail, hlasova linka a formular jsou plnohodnotne spoustece. Automatizace zacne prichozi zpravou a skonci zalozenym ticketem. **Ticket ma sveho cloveka.** `assignee` neni volny text, ale odkaz na konkretniho resitele. Da se rict "hod to na Karla Vomacku" a Karel to ma mezi svymi tickety. Nad tym je prehled pres cely tym, kde je videt, kdo co u sebe ma. **Kazdy ticket je dohledatelny.** Nese si strom zaznamu o tom, co se s nim delo a **co ktera sluzba vratila**. Kdyz neco nesedi, neni potreba hadat. ## Datovy model `src/data/ticketStore.ts` ```ts interface Ticket { id: string; subject: string; body: string; // cely text pozadavku, prazdny = krok ho nenaplnil sourceRef: string | null; // odkaz na zdrojovou zpravu u poskytovatele channel: 'whatsapp' | 'email' | 'voice' | 'form' | 'portal'; customer: TicketCustomer; status: 'new' | 'open' | 'waiting' | 'resolved'; priority: 'low' | 'normal' | 'high' | 'critical'; assignee: { id: string; name: string } | null; // null = ceka ve fronte automationId: string | null; // null = zalozeno rucne createdAt: string; updatedAt: string; } interface TicketCustomer { id: string | null; // null = firmu se nepodarilo dohledat v CRM company: string; contact: string; reply: string; // adresa nebo cislo, kam se odpovida } ``` `customer.id` je zamerne nullable. Prave na nej se pta podminka "mame zakaznika?" ve strome automatizace. Bez toho by nesla postavit vetev "firmu neznam, zaloz obchodni pripad a nekomu to dej". `subject` je kratke shrnuti do seznamu, `body` je cely text pozadavku. Do `body` patri telo e-mailu, zprava z WhatsApp nebo prepis hovoru. Kdyz je prazdne, znamena to, ze krok "Zalozit ticket" nemel nastavene pole Obsah - detail ticketu to napise nahlas misto toho, aby ukazal prazdne misto. Uvnitr ulozista se drzi jen `assigneeId`, jmeno se dopocitava pri cteni. Kdyz resitel ze seznamu zmizi, ticket nespadne - jen se zaloguje a tvari se jako neprirazeny. ## Resitele `src/data/people.ts` Resitel je oddeleny od uzivatele. **Uzivatel** je ten, kdo se prihlasi do portalu, **resitel** je ten, na koho jde ticket. Casto je to tyz clovek, ale ne vzdy - technik muze mit tickety a do portalu se nikdy neprihlasit. Spojka mezi obojim je e-mail. Podle ni funguje filtr "moje tickety" (`?assignee=me`). Kdyz prihlaseny ucet zadnemu resiteli neodpovida, filtr se v portalu nabidne jako nedostupny misto toho, aby vracel prazdno bez vysvetleni. Kazdy resitel ma `capacity`, tedy pocet nevyrizenych ticketu, ktery je pro nej jeste zdrava zatez. Neni to limit, nic se podle nej neodmita - jen se v prehledu oznaci, kdo je nad ni. ## Prehled nad firmou `GET /api/dashboard/tickets/workload` vraci pres cely tym: kolik ma kdo nevyrizenych, kolik celkem, kolik kritickych, jak stary je jeho nejstarsi ticket a jestli je nad kapacitu. K tomu pocet ticketu ve fronte bez resitele. V portalu je to postranni panel na strance Tickety. Radek je zaroven filtr seznamu - kliknutim na cloveka se seznam zuzi na jeho tickety. Bez toho by to byl jen obrazek. Sirka pruhu se pocita proti nejvytizenejsimu clenovi tymu, ne proti kapacite. Jde o porovnani lidi mezi sebou. ## Log ticketu Log je **strom**, ne seznam. Vetev podminky visi na zaznamu te podminky, takze je videt i to, ktera cast behu se vubec nespustila. ```ts interface TicketTraceEntry { id: string; parentId: string | null; // null = hlavni sekvence kind: 'trigger' | 'action' | 'condition' | 'note'; connectorId: string | null; // ktera sluzba to byla operationId: string | null; label: string; status: 'ok' | 'error' | 'skipped' | 'info'; response: string | null; // co sluzba vratila durationMs: number | null; at: string; } ``` `response` je duvod, proc log existuje. V portalu se zobrazuje rovnou, ne po rozkliknuti - kvuli nemu se do logu chodi. Zapisuje se zanorene (`TraceInput` s `children`) a uklada zplostele s `parentId`. Klient si strom zase poskladá. Zaznam s neznamym rodicem se nezahodi, prida se do korene a zaloguje - ztratit radek logu je horsi nez ho ukazat spatne zanoreny. Komentare jsou taky zaznamy logu (`kind: 'note'`). Diky tomu je vsechno na jedne casove ose a nemusi se nikde skladat dohromady dva ruzne seznamy. ## Konektor Tickety Kategorie `servicedesk`, driv byl pod Nastroji. Ma obe strany: | Spoustec | Kdy | | ------------------ | ------------------------------------------------------ | | `created` | zalozen ticket, at uz z kanalu nebo rucne | | `unknown-customer` | k ticketu se nepodarilo dohledat firmu | | `assigned` | ticket dostal konkretniho cloveka | | `status-changed` | prechod do jineho stavu vcetne vyreseni | | Akce | Co dela | | --------------- | --------------------------------------------- | | `create` | zalozi pozadavek | | `assign` | preda ticket cloveku | | `set-status` | posune stav | | `link-customer` | doplni firmu z CRM | | `comment` | zapise komentar do logu | Typicky retez, ktery z toho jde postavit: ``` Prijata zprava z WhatsApp Zaradit do kategorie (AI) Dohledat firmu podle telefonu (CRM) knownCustomer? ANO Zalozit ticket, Priradit resiteli NE Zalozit ticket, Zalozit obchodni pripad, Upozornit servicedesk ``` ## Co se ticketu preda Akce "Zalozit ticket" ma nastavitelna pole. Klikni na krok ve strome a vyplnis je primo tam. Hodnota je **sablona**: `{{nazev}}` se nahradi parametrem spoustece. | Pole | Typicka hodnota u WhatsApp | | ------------ | --------------------------------- | | Predmet | `Zprava od {{profileName}}` | | Obsah | `{{text}}` | | Firma | necha se prazdne, doplni CRM krok | | Kontakt | `{{profileName}}` | | Odpoved na | `{{phone}}` | | Priorita | vyber ze seznamu | | Resitel | vyber ze seznamu lidi | Nabidka parametru je pod poli. Kliknuti vlozi `{{nazev}}` na pozici kurzoru, takze se nemusi psat rucne a neudela se preklep. Vyber resitele se plni ze seznamu v `people.ts`, ne z rucne psaneho ID. Novy clovek v tymu se v nabidce objevi sam. ## Co je kde videt Krok vidi **parametry spoustece plus vystupy vsech kroku, ktere jsou pred nim**. Diky tomu jde do stromu vlozit predvalidaci a vetvit se podle jeji odpovedi. Dve pravidla: - krok vidi to, co je pred nim ve stejne sekvenci, a to, co videl jeho rodic, - **vetev podminky nepridava nic do sekvence za podminkou**. Vetev nemusela probehnout, spolehat se na jeji vystup by byla past. Vystupy akci deklaruje katalog v `outputFields`. Ma je napriklad "Dohledat firmu" (`customerKnown`, `companyId`, `companyName`), "Zaradit do kategorie" (`category`, `confidence`) nebo "Zalozit ticket" (`newTicketId`). Vypocet je v `src/data/flowScope.ts`, kopie pro UI v `web/src/lib/flow.ts`. ## Jak udelat podminku Tri ruzne pripady, kazdy se resi jinak. **Na prichozi zpravu.** `text` je parametr spoustece, funguje rovnou: ``` Prijata zprava z WhatsApp text obsahuje "faktura" ANO ... ``` **Na vysledek mezikroku.** Tohle je ta predvalidace. Krok "Dohledat firmu" vrati `customerKnown` a podminka se na nej zepta: ``` Prijata zprava z WhatsApp Dohledat firmu Telefon {{phone}} customerKnown je splneno ANO Zalozit ticket Firma {{companyName}}, Obsah {{text}} NE Zalozit ticket bez firmy Zalozit obchodni pripad ``` `{{companyName}}` je uvnitr vetve dostupne, protoze krok "Dohledat firmu" je pred podminkou. Presne tenhle strom je ve vzorovych automatizacich. **Na obsah uz zalozeneho ticketu.** Ve stejnem strome nejde, ticket v tu chvili teprve vznika a "Zalozit ticket" vraci jen `newTicketId`, ne cely ticket. Dela se **druhou automatizaci** se spoustecem "Zalozen ticket": ``` Zalozen ticket assigned neni splneno ANO body obsahuje "faktur" ANO Priradit resiteli ID {{ticketId}}, Martin Kriz NE body obsahuje "voicebot" ANO Priradit Eva Novakova NE Priradit Karel Vomacka ``` Rozdeleni neni obchazeni omezeni, ale zamer. Prijem z kanalu je jedna automatizace na kanal, smerovani je jedna spolecna nad vsemi tickety. ## Vzorove automatizace V `automationStore.ts` jsou nasazene presne v tomhle rozdeleni: | Automatizace | Co ukazuje | | -------------------------------- | --------------------------------------------- | | WhatsApp: zprava do ticketu | predvalidace v CRM a vetveni podle vysledku | | Facebook: zprava do ticketu | prijem bez predvalidace, nemame podle ceho hledat | | E-mail: pozadavky do ticketu | dohledani podle `{{from}}`, plus odpoved zadavateli | | Smerovani ticketu na resitele | jedna spolecna logika nad vsemi tickety | Prijmove automatizace zamerne **neprirazuji resitele**. Nechavaji ticket ve fronte a smerovani si ho prevezme. Podminka `assigned neni splneno` na zacatku smerovani zaridi, ze rucni prirazeni se neprepise. ## Sablony Odkazuje se **jmenem** parametru (`{{subject}}`), ne jeho ID. Jmeno uzivatel vidi a pise, `{{f_42}}` by nikdo neprecetl. Cenou je, ze prejmenovani parametru sablonu rozbije. Proto se to hlida: - builder podtrhne pole a napise, ktery parametr chybi, - server to vraci mezi `issues`, tedy jako **nedodelek**, ne jako chybu. Rozdelana prace se ulozi, jen automatizace nepujde zapnout. Naopak **chyba (400)** je nastaveni pole, ktere akce v katalogu nema. To uz neni nedodelek, ale rozbity strom - ulozit ho by znamenalo drzet data, ktera nikdo neprecte. Chybejici parametr se pri behu dosadi jako prazdny retezec a zaloguje. Nechat v textu `{{neco}}` by znamenalo poslat to zakaznikovi. Akce, ktere `inputs` zatim nemaji, to v builderu napisou primo na karte kroku. Lepsi nez nechat cloveka hledat nastaveni, ktere neexistuje. ## Parametry od sluzby Nektere spoustece si data urcuji samy. E-mail posila `from`, `subject`, `body`. WhatsApp posila `phone`, `text`. Ticket posila `knownCustomer`. Uzivatel si je nevymysli, ale potrebuje nad nimi stavet podminky. Katalog je proto deklaruje v `providedFields` u operace. Chovaji se pak takhle: - builder je ukazuje **jen ke cteni**, pridat ani prejmenovat nejdou, - server je pri ulozeni stromu **vzdy dosadi z katalogu** a to, co poslal klient, zahodi (`normalizeTriggerFields` v `src/routes/dashboard.ts`), - dosazeni probiha **pred validaci**, jinak by podminky odkazujici na katalogova ID vypadaly jako rozbite. ID techto parametru (`email.from`, `ticket.knownCustomer`) musi zustat stabilni. Odkazuji se na ne podminky v ulozenych stromech, prejmenovani ID je rozbije. Spoustece bez `providedFields` (webhook, formular) funguji jako predtim - parametry si deklaruje uzivatel. ## Stranky portalu ``` /dashboard/tickety seznam, filtry, prehled vytizeni /dashboard/tickety/:id detail: prubeh a log, resitel, zakaznik, puvod ``` ## Otevrene rozhodnuti pro runtime Model nema zadne "prvni shoda vyhrava". **Vsechny automatizace se stejnym spoustecem se spusti, vsechny.** Podminka je uvnitr stromu, ne na spousteci, takze automatizaci nezastavi pred prvnim krokem. Doporucene rozdeleni (jedna automatizace na kanal, jedna spolecna na smerovani) na to nenarazi, protoze kazdy prijem ma jiny spoustec a smerovani je jen jedno. Problem vznikne, jakmile nekdo udela vic automatizaci nad stejnym spoustecem: - tri automatizace na "Prijat e-mail", kazda s jinym `obsahuje`, udelaji z jednoho e-mailu tri tickety, - dve smerovaci automatizace provedou dve prirazeni a poradi neni dane. Dnes se to neprojevi, protoze runtime neexistuje. Az se bude psat, je potreba to rozhodnout vedome, ne omylem. Varianty od nejlevnejsi: 1. **Nechat jak je** a drzet se pravidla jedna automatizace na spoustec. Funguje dnes, nic se nemeni, ale nikdo to nevynucuje. 2. **Filtr na spousteci.** Automatizace by umela rict "tenhle e-mail neni muj" jeste pred prvnim krokem. Umozni jednu automatizaci na pravidlo. 3. **Poradi a prvni shoda vyhrava.** Nejmocnejsi, ale chovani zavisle na neviditelnem poradi se spatne ladi. Nedoporucuje se. ## Co chybi | Chybi | Poznamka | | ---------------------------- | ------------------------------------------------------- | | Bugs a wishes | vyvojarska agenda, samostatna evidence | | Skutecny beh automatizaci | sablony se ukladaji, ale nikdo je nevyhodnocuje | | `inputs` u zbylych konektoru | zatim ticket, kanaly, CRM a AI, ostatni maji jen napovedu | | Napojeni logu na beh | `automationId` je odkaz, historie behu ale neexistuje | | Odpoved zakaznikovi z detailu| akce `send` u kanalu se z portalu nevola | | SLA a eskalace | zadne lhuty, `capacity` je jen orientacni | | Databaze | data v pameti, restart je vrati na vychozi sadu |