Krok mel v poli Obsah nastaveno {{rating}}. Data v behu prokazatelne byla,
v udalostech ticketu je hodnoceni videt cele, ale ticket zustal s prazdnym
obsahem.
intakeEvent deli praci na zalozeni a navazani na existujici ticket a vsechno
z `create` platilo jen pro tu prvni vetev. U existujiciho ticketu se doplnovaly
pouze vlastni pole a stitky, zbytek se tise zahodil. U hovoru to znamena, ze
obsah nedorazi nikdy: prvni zprava jen oznami, ze hovor zacal (in-progress,
data null), a prave ta ticket zaklada. Hodnoceni prijde az posledni zpravou,
kdy uz ticket existuje. Stav byl jedina vyjimka, protoze ho krok nastavuje
zvlast pres updateTicketStatus - proto fungoval a zbytek ne.
Jedno pravidlo misto dvou seznamu poli:
- neprazdna hodnota prepise, prazdna nemaze. IntakeInput ma na to `apply`,
v `create` zustala jen zaloha predmetu a vychozi stav
- prazdna hodnota nemaze schvalne. Prave to byla puvodni obava, kvuli ktere se
zapisovalo jen pri zalozeni: pozdejsi zprava bez jmena zakaznika je bezna
a smazat kvuli ni jmeno by bylo horsi nez ho nedoplnit
- vyjimky zustavaji dve: zaloha predmetu z externiho ID plati jen pri vzniku
a stav chodi pres updateTicketStatus, ktere resi i priznak vyrizeni, cas
vyreseni a pocet znovuotevreni
Data smi chodit po castech:
- vlastni pole typu se scitaji podle klicu. Prvni zprava posle `data`, druha
`data2` a ticket ma obe
- prazdny retezec pole nemaze. Sablona, ktera na nic neukazuje, se dosadi
prazdnem, takze {"vysledek":"{{result}}"} u zpravy bez vysledku posilalo
prazdno a prepsalo tim hodnotu z minule zpravy. Vymazat pole jde poslanim
null, coz uz je zamer
Dalsi dve veci, ktere u toho vyplavaly:
- create.status se do createTicket vubec nepredaval, takze ticket vznikl
s vychozim "Nový" a hned se prepsal. V logu pak stalo "stav Nový ->
completed" u ticketu, ktery v nem nikdy nebyl
- faze byla zrusena uz driv, ale v katalogu po ni zbyval krok "Posunout do
dalsi faze" a pole Faze u zalozeni ticketu. Ticket ani typ ticketu fazi
nemaji, takze krok by selhal na chybejicim skriptu a pole se zahazovalo.
Oboji je pryc
Krok navic v logu rekne, co doplnil: "doplnen TK-123, stav completed, obsah".
Driv radek jen oznamil, ze se ticket doplnil, a nebylo poznat cim.
Overeno na bezici instanci s vlastnim DATA_DIR, tremi zpravami o jednom hovoru:
prvni zaklada ticket s prazdnym obsahem, druha doplni obsah i zakaznika, treti
bez dat je nechava byt a meni jen stav. Scenar s `data` a pak `data2` ma na konci
obe hodnoty.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
18 KiB
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
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.
body se zapisuje pri kazde udalosti, ne jen pri zalozeni, stejne jako
skoro vsechno ostatni - podrobnosti nize v "Zalozeni a doplneni delaji totez".
Cela historie zustava v udalostech ticketu, at uz je v body cokoliv.
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.
Fronta skupiny a prevzeti
Ticket nemusi mit resitele hned. Muze lezet u skupiny a cekat, az si ho
nekdo vezme - POST /tickets/:id/claim. Kdo je ve skupine, si ho vezme sam,
prace se nerozdava shora.
Proc obojí vedle sebe:
| Situace | Co se hodi |
|---|---|
| havarie, musi to nekdo hned resit | automat prideli nejvolnejsimu |
| bezny dotaz, lidi maji ruzne dny | necha se ve fronte skupiny |
Proto je prepinac Priradit rovnou nejvolnejsimu na kroku automatizace
(ticket/assign-group), ne na skupine: tataz skupina potrebuje obe chovani,
jen u jineho typu prace.
Vzit si ticket, ktery uz nekdo resi, neni prevzeti, ale prehozeni. Chce to pravo
ticket.assign.others a jde to jen pres prirazeni, ne pres prevzeti.
Pozvanky do firmy
src/data/invites.ts
Novy clovek se do firmy dostane odkazem s nahodnym kodem, ne tim, ze mu nekdo zalozi ucet a posle heslo. Duvody:
- heslo nikdy nikam neposilame. Kdo si ho nastavi sam za odkazem, ma ho jen on,
- pozvanka vyprsi (tyden) a da se zrusit, ucet ne,
- kdo uz ucet ma, se jen pripoji k dalsi firme misto zakladani druheho - ale musi zadat svoje heslo, jinak by kdokoliv s odkazem pripojil cizi adresu ke sve firme.
U pozvanky se rovnou rekne, jake role clovek dostane a jestli z nej ma byt i resitel. Uzivatel a resitel nejsou totez, viz vyse - proto se to pta misto hadani.
Sprava je v zalozce Lide, ne v nastaveni: pozvat kolegu je bezna denni prace.
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.
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.
Opakovana udalost
Odesilatele umi poslat stejnou zpravu i osmdesatkrat za minutu. Kdyz prijde presne totez co posledne (stejny typ, zdroj, popisek a stejna data), nezaklada se dalsi radek: pricte se k pocitadlu u te predchozi.
| Co se stane | Proc |
|---|---|
repeats +1, lastAt = ted |
osmdesat radku znamena, ze v historii nikdo nic nenajde |
| ticket se nemeni | jinak by duplikat rozblikal dashboard a spustil automatizaci na zmenu ticketu |
| do logu se nezapisuje nic | log ma ukazovat zmeny, ne to, ze se nezmenilo nic |
| udalost se nezahazuje | bez pocitadla by nikdo nezjistil, ze proti nam neco tluce ve smycce |
Porovnava se jen s posledni udalosti. "Objednavka pripravena" muze legitimne prijit znovu za hodinu, kdyz se mezitim stalo neco jineho - to je novy fakt.
Co je v logu videt
Log se cte jako prislo tohle a zpusobilo to tuhle zmenu. Zmeny, ktere udela
beh automatizace, se radi pod jeho radek udalosti (parentId), takze nejde
zamenit, co zpusobilo co:
Prijata udalost: stav completed (automatizace TEST) 13:13:27
stav in-progress -> completed 13:13:27
stitky +hovor 13:13:27
Prijata udalost: stav completed 22x beze zmeny 13:13:34
U udalosti stoji, kdo ji prinesl. Prvni otazka nad zmenenym ticketem je
"kdo mi do toho sahl" a odpoved webhook na ni neodpovida.
Data udalosti se ukladaji cela. Kdyz krok nema vlastni, ulozi se to, cim beh zacal - u webhooku prijate telo. Prazdna udalost je horsi nez zadna: tvari se, ze se neco stalo, a nerekne co.
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.
Zalozeni a doplneni delaji totez
Kdyz uz ticket se stejnym externim ID existuje, zapise se do nej totez co pri zalozeni. Plati jedno pravidlo:
Neprazdna hodnota prepise, prazdna nemaze.
Diky tomu nemusi data prijit najednou. U hovoru prvni zprava jen ohlasi, ze zacal, a hodnoceni dorazi az posledni - ta uz na ticket dopadne, i kdyz ticket zalozila ta prvni.
| Pole | Chovani u druhe a dalsi zpravy |
|---|---|
| Predmet, Obsah | neprazdna hodnota prepise |
| Typ, Priorita, Kanal | neprazdna hodnota prepise |
| Zakaznik, Resitel, Skupina | neprazdna hodnota prepise, po polozkach |
| Stav, Vyrizeny | prepise se, pres updateTicketStatus |
| Stitky | pridaji se, existujici zustanou |
| Vlastni pole typu | klice se scitaji, prazdna hodnota nemaze |
Dve vyjimky, obe zamerne:
- Zaloha predmetu (externi ID, kdyz zadny predmet nezadate) se dosadi jen pri vzniku. Jinak by se predmet prepisoval externim ID i tam, kde ho nikdo nechtel.
- Stav nechodi tudy, ale pres
updateTicketStatus. To resi i priznak vyrizeni, cas vyreseni a pocet znovuotevreni - obejit ho by ta cisla rozbilo.
Vlastni pole typu se scitaji podle klicu: kdyz prvni zprava prinese data
a druha data2, ma ticket obe. Prazdny retezec nemaze, protoze sablona, ktera
na nic neukazuje, se dosadi prazdnem. Vymazat pole jde poslanim null - to uz
je zamer, ne vedlejsi ucinek nevyplnene sablony.
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 (
normalizeTriggerFieldsvsrc/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:
- Nechat jak je a drzet se pravidla jedna automatizace na spoustec. Funguje dnes, nic se nemeni, ale nikdo to nevynucuje.
- Filtr na spousteci. Automatizace by umela rict "tenhle e-mail neni muj" jeste pred prvnim krokem. Umozni jednu automatizaci na pravidlo.
- 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 |