updated tickets

This commit is contained in:
JiriUhlir
2026-08-03 11:45:35 +02:00
parent 9a0c67af08
commit 52b190bfbc
28 changed files with 3228 additions and 241 deletions
+185
View File
@@ -0,0 +1,185 @@
# 06 - Tickety
## Co ticket je a co neni
Ticket je **prichozi pozadavek odkudkoliv**. Prisla zprava na WhatsApp, 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;
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".
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
```
## 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
```
## Co chybi
| Chybi | Poznamka |
| ---------------------------- | ------------------------------------------------------- |
| Bugs a wishes | vyvojarska agenda, samostatna evidence |
| Skutecny beh automatizaci | log ticketu zatim plni simulace, ne runtime |
| 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 |