Zalozit nebo doplnit ticket: doplneni konecne doplnuje
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
35d1e43307
commit
1134852bff
@@ -60,6 +60,10 @@ 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.
|
||||
@@ -249,6 +253,39 @@ spoustece.
|
||||
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.
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ Volající nikdy nezjišťuje, jestli běží Postgres, soubor, nebo pamět.
|
||||
| `notify(input)` | `src/data/notifications.ts` | Upozorní člověka. Nečeká se a nevyhazuje chyby, stejně jako audit. |
|
||||
| `runFlow(steps, context, options)` | `src/runtime/executor.ts` | Vykoná strom kroků. Nikdy nevyhodí výjimku, chyba je výsledek. Používá to akce na ticketu i webhook, aby se strom choval všude stejně. |
|
||||
| `widgetCatalog(tenantIds, userId)` | `src/data/widgets.ts` | Jediná definice toho, co jde položit na dashboard. Používá ji nabídka i kontrola ukládaného rozložení. |
|
||||
| `intakeEvent(input)` | `src/data/ticketStore.ts` | Přijme událost zvenku: podle externího ID buď založí ticket, nebo ji navěsí na existující. Jediná cesta, kterou se událost stává ticketem. |
|
||||
| `intakeEvent(input)` | `src/data/ticketStore.ts` | Přijme událost zvenku: podle externího ID buď založí ticket, nebo ji navěsí na existující. Jediná cesta, kterou se událost stává ticketem. Hodnoty z `input.apply` zapíše v obou případech, prázdné nemaže. |
|
||||
| `getAgentStats(...)` | `src/data/ticketStore.ts` | Výkon řešitelů: odbavené, mediány časů, vrácené, fronta. Používá to widget i detail osoby, aby čísla seděla. |
|
||||
| `findByExternalId(...)` | `src/data/ticketStore.ts` | Ticket firmy podle externího ID. Klíč je dvojice firma a ID. |
|
||||
| `findByIntakeToken(token)` | `src/data/tenants.ts` | Firma podle tokenu příjmu. Určuje i to, v jakém rozsahu je externí ID unikátní. |
|
||||
|
||||
@@ -149,7 +149,6 @@ operace jako každá jiná.
|
||||
| `ticket/upsert` | podle externího ID založí ticket, nebo na existující navěsí událost |
|
||||
| `ticket/assign-least-busy` | předá nejvolnějšímu ze skupiny, při shodě rozhoduje podíl ke kapacitě |
|
||||
| `ticket/set-type` | nastaví typ, za kterým stojí vlastní pole |
|
||||
| `ticket/set-stage` | posune do další fáze workflow daného typu |
|
||||
| `ticket/add-tags` | přidá štítky, existující nechá |
|
||||
| `ticket/set-status` | změní stav v životním cyklu |
|
||||
| `incident/create` | založí incident |
|
||||
@@ -157,18 +156,20 @@ operace jako každá jiná.
|
||||
|
||||
## Tři osy na ticketu
|
||||
|
||||
| Osa | Kdo ji určuje | K čemu |
|
||||
| -------- | ----------------------------------------------- | ------------------------------------------------------ |
|
||||
| `status` | pevná čtveřice (nový, v řešení, čeká, vyřešeno) | životní cyklus, počítají se z něj statistiky a fronta |
|
||||
| `stage` | firma u typu ticketu (`TicketType.statuses`) | postup uvnitř typu: čeká na zabalení, předáno dopravci |
|
||||
| `tags` | kdokoliv, volně | označení, která spolu nemusí souviset |
|
||||
| Osa | Kdo ji určuje | K čemu |
|
||||
| -------- | ---------------------------------------------- | -------------------------------------------------------- |
|
||||
| `status` | odesílatel, nabídku dává `TicketType.statuses` | kde ticket je: čeká na zabalení, předáno dopravci |
|
||||
| `closed` | výslovně, krok nebo člověk | co už nikdo neřeší, z toho se počítá fronta a statistiky |
|
||||
| `tags` | kdokoliv, volně | označení, která spolu nemusí souviset |
|
||||
|
||||
Fáze může být **jen jedna**, proto se na ni dá spolehnout v podmínce. Přes
|
||||
Stav může být **jen jeden**, proto se na něj dá spolehnout v podmínce. Přes
|
||||
štítky by to fungovalo taky, ale ticket by mohl mít "čeká na zabalení"
|
||||
i "expedováno" naráz a nikdo by nepoznal, co platí.
|
||||
|
||||
Fáze mimo workflow typu se odmítne. Překlep by jinak tiše vyřadil podmínku,
|
||||
která na fázi stojí.
|
||||
Fáze jako třetí osa tady byla a **je zrušená**. Když je stav volný řetězec,
|
||||
je druhé pole na tutéž věc jen zmatení. V katalogu po ní zbýval krok
|
||||
"Posunout do další fáze" a pole Fáze u založení ticketu, jenže model fázi
|
||||
neměl - krok neměl co vykonat a pole se tiše zahazovalo.
|
||||
|
||||
## Živý dashboard
|
||||
|
||||
|
||||
@@ -2,6 +2,79 @@
|
||||
|
||||
Nejnovejsi nahore.
|
||||
|
||||
## 2026-09-02 - Zalozit NEBO DOPLNIT ticket: doplneni konecne doplnuje
|
||||
|
||||
Automatizace mela v kroku "Zalozit nebo doplnit ticket" pole Obsah nastavene na
|
||||
`{{rating}}`. Data v behu prokazatelne byla - v udalostech ticketu je hodnoceni
|
||||
videt cele - ale ticket zustal s prazdnym obsahem.
|
||||
|
||||
### Cim to bylo
|
||||
|
||||
`intakeEvent` deli praci na **zalozeni** a **navazani na existujici ticket**.
|
||||
Vsechno z `create` platilo jen pro tu prvni vetev. U existujiciho ticketu se
|
||||
doplnovaly pouze vlastni pole a stitky. Predmet, obsah, typ, priorita, kanal,
|
||||
zakaznik, resitel ani skupina ne - tise se zahodily.
|
||||
|
||||
U hovoru to znamena, ze obsah nedorazi nikdy. Prvni zprava jen oznami, ze hovor
|
||||
zacal (`status: in-progress`, `data: null`), a **prave ta ticket zaklada**, tedy
|
||||
s prazdnym obsahem. 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, a proto to vypadalo jako chyba
|
||||
jednoho pole.
|
||||
|
||||
### Jedno pravidlo misto dvou seznamu
|
||||
|
||||
Zalozeni a doplneni ted delaji totez:
|
||||
|
||||
> Neprazdna hodnota prepise, prazdna nemaze.
|
||||
|
||||
`IntakeInput` ma na to `apply`, v `create` zustal jen zaloha predmetu a vychozi
|
||||
stav. Dva ruzne seznamy poli by se stejne zase rozesly a nekde by zas neco
|
||||
chybelo.
|
||||
|
||||
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. Nove se prepise jen to,
|
||||
co odesilatel opravdu poslal, takze pojistka plati a data se neztraci.
|
||||
|
||||
Dve vyjimky zustavaji: 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. Odesilatel se nemusi predem dohodnout, co vsechno
|
||||
posle, a nemusi posilat vsechno pokazde.
|
||||
|
||||
Prazdny retezec pritom 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`: to uz je zamer, ne vedlejsi ucinek nevyplnene sablony.
|
||||
|
||||
### Stav se ted ulozi uz pri vzniku
|
||||
|
||||
`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 "Nový" nikdy nebyl. Komentar u volajiciho tvrdil, ze uz je to
|
||||
opravene - opravena byla jen jedna strana.
|
||||
|
||||
### Faze dorazena do konce
|
||||
|
||||
Faze byla zrusena uz driv, hodnoty z ni patri do stavu. V katalogu po ni ale
|
||||
zbyval krok "Posunout do dalsi faze" a pole Faze u zalozeni ticketu. Ani jedno
|
||||
nemelo co delat: ticket ani typ ticketu fazi nemaji. Krok by pri behu selhal na
|
||||
chybejicim skriptu a pole se tise zahazovalo, takze `{{status}}` napsany do Faze
|
||||
nedelal nic. Oboji je pryc.
|
||||
|
||||
### Aby bylo videt, ze se to ulozilo
|
||||
|
||||
Krok v logu rekne, co doplnil: `doplnen TK-123, stav completed, obsah`. Driv
|
||||
radek jen oznamil, ze se ticket doplnil, a nebylo poznat cim - u dat, ktera
|
||||
dorazi az druhou zpravou, je to zrovna ta informace, kterou clovek hleda.
|
||||
|
||||
|
||||
## 2026-08-28 - pad portalu uz nesmi shodit stranku a zaklada incident
|
||||
|
||||
Ukazka tela webhooku shazovala cely builder pri psani cesty parametru. Chyba
|
||||
|
||||
+20
-31
@@ -535,7 +535,8 @@ export const services: Service[] = [
|
||||
description:
|
||||
'Podle externího ID buď založí nový ticket, nebo na existující navěsí událost. ' +
|
||||
'Externí ID je unikátní v rámci firmy, takže druhá zpráva o téže objednávce ' +
|
||||
'skončí na jednom místě.',
|
||||
'skončí na jednom místě. Co je tady vyplněné, zapíše se i na existující ticket - ' +
|
||||
'prázdná hodnota nikdy nic nesmaže, takže data můžou chodit po částech.',
|
||||
implementation: 'script',
|
||||
inputs: [
|
||||
{
|
||||
@@ -546,7 +547,13 @@ export const services: Service[] = [
|
||||
hint: 'ID u odesílatele, typicky číslo objednávky. Bez něj vznikne vždy nový ticket.',
|
||||
},
|
||||
{ id: 'subject', label: 'Předmět', kind: 'text', required: false },
|
||||
{ id: 'body', label: 'Obsah', kind: 'longtext', required: false },
|
||||
{
|
||||
id: 'body',
|
||||
label: 'Obsah',
|
||||
kind: 'longtext',
|
||||
required: false,
|
||||
hint: 'Text může dorazit až druhou zprávou, doplní se i na existující ticket.',
|
||||
},
|
||||
{
|
||||
id: 'typeId',
|
||||
label: 'Typ ticketu',
|
||||
@@ -594,26 +601,21 @@ export const services: Service[] = [
|
||||
],
|
||||
hint: 'Prázdné = nechat, jak je. Podle tohohle se počítá fronta a statistiky.',
|
||||
},
|
||||
{
|
||||
id: 'stage',
|
||||
label: 'Fáze',
|
||||
kind: 'text',
|
||||
required: false,
|
||||
hint: 'Musí být z workflow daného typu. Například stav hovoru od voicebota.',
|
||||
},
|
||||
{
|
||||
id: 'fields',
|
||||
label: 'Vlastní pole typu',
|
||||
kind: 'json',
|
||||
required: false,
|
||||
hint: 'JSON s klíči podle typu ticketu, například {"orderNumber":"{{orderId}}"}.',
|
||||
hint:
|
||||
'JSON s klíči podle typu ticketu, například {"orderNumber":"{{orderId}}"}. ' +
|
||||
'Klíče se sčítají: co přinesla minulá zpráva, zůstane.',
|
||||
},
|
||||
{
|
||||
id: 'company',
|
||||
label: 'Zákazník: firma',
|
||||
kind: 'text',
|
||||
required: false,
|
||||
hint: 'Vyplní se jen při založení, další událost to nepřepíše.',
|
||||
hint: 'Prázdná hodnota jméno nesmaže, takže zpráva bez firmy nic nepokazí.',
|
||||
},
|
||||
{ id: 'contact', label: 'Zákazník: kontakt', kind: 'text', required: false },
|
||||
{
|
||||
@@ -795,26 +797,13 @@ export const services: Service[] = [
|
||||
{ id: 'ticketId', label: 'Ticket', kind: 'text', required: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'set-stage',
|
||||
name: 'Posunout do další fáze',
|
||||
description:
|
||||
'Fáze je vlastní workflow typu ticketu, například čeká na zabalení nebo ' +
|
||||
'předáno dopravci. Na rozdíl od štítků může být jen jedna, takže se na ni ' +
|
||||
'dá spolehnout v podmínce.',
|
||||
implementation: 'script',
|
||||
inputs: [
|
||||
{
|
||||
id: 'stage',
|
||||
label: 'Fáze',
|
||||
kind: 'text',
|
||||
required: true,
|
||||
hint: 'Musí být z workflow daného typu, jinak krok selže.',
|
||||
},
|
||||
{ id: 'ticketId', label: 'Ticket', kind: 'text', required: false },
|
||||
],
|
||||
outputFields: [{ id: 'stage', name: 'stage', type: 'string', required: true }],
|
||||
},
|
||||
/*
|
||||
* "Posunout do dalsi faze" tady bylo, ale fazi nema ani ticket, ani typ
|
||||
* ticketu - v modelu nikdy nevznikla. Krok nemel co vykonat a pole Faze
|
||||
* u zalozeni ticketu se tise zahazovalo. To, co mela faze delat, uz umi
|
||||
* stav: je prave jeden, je to volny retezec a typ ticketu si k nemu muze
|
||||
* nabidnout svoje hodnoty.
|
||||
*/
|
||||
{
|
||||
id: 'set-status',
|
||||
name: 'Změnit stav ticketu',
|
||||
|
||||
+129
-14
@@ -236,7 +236,6 @@ interface StoredTicket
|
||||
| 'typeId'
|
||||
| 'fields'
|
||||
| 'tags'
|
||||
| 'stage'
|
||||
| 'assigneeGroupId'
|
||||
| 'externalId'
|
||||
| 'closed'
|
||||
@@ -1080,6 +1079,26 @@ export function findByExternalId(tenantId: string, externalId: string): Ticket |
|
||||
return stored ? toTicket(stored) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Co jde na ticketu nastavit prichozi udalosti.
|
||||
*
|
||||
* Zakaznik je rozepsany po polozkach schvalne. Kdyby se predaval cely, prepsala
|
||||
* by zprava, ktera zna jen telefon, i jmeno firmy - prazdnou hodnotou.
|
||||
*/
|
||||
export interface TicketApply {
|
||||
subject?: string;
|
||||
body?: string;
|
||||
typeId?: string;
|
||||
priority?: TicketPriority;
|
||||
channel?: TicketChannel;
|
||||
sourceRef?: string;
|
||||
company?: string;
|
||||
contact?: string;
|
||||
reply?: string;
|
||||
assigneeId?: string;
|
||||
assigneeGroupId?: string;
|
||||
}
|
||||
|
||||
export interface IntakeInput {
|
||||
tenantId: string;
|
||||
/** Bez nej se navazani nema o co oprit a zalozi se novy ticket. */
|
||||
@@ -1091,8 +1110,28 @@ export interface IntakeInput {
|
||||
label?: string;
|
||||
/** Cela prijata data. */
|
||||
payload?: Record<string, unknown>;
|
||||
/** Pouzije se jen kdyz ticket teprve vznika. */
|
||||
create?: Partial<Omit<CreateTicketInput, 'tenantId' | 'externalId' | 'externalSource'>>;
|
||||
/**
|
||||
* Hodnoty, ktere maji smysl **jen pri vzniku** ticketu.
|
||||
*
|
||||
* Zbylo jich malo a je to zamer: predmet jako zaloha, kdyz ho odesilatel
|
||||
* neposlal, a vychozi stav. Vsechno ostatni patri do `apply`.
|
||||
*/
|
||||
create?: Partial<Pick<CreateTicketInput, 'subject' | 'status' | 'fields' | 'tags' | 'automationId'>>;
|
||||
/**
|
||||
* Hodnoty, ktere se zapisi **pri kazde udalosti**, ne jen pri zalozeni.
|
||||
*
|
||||
* Driv bylo skoro vsechno jen pro vznik ticketu a nedava to smysl: krok se
|
||||
* jmenuje "zalozit **nebo doplnit**" a data casto nechodi najednou. Prvni
|
||||
* zprava jen oznami, ze se neco deje - u hovoru nese cislo a `in-progress`,
|
||||
* a prave ta ticket zaklada. Predmet, obsah, typ i zakaznik dorazi az tou
|
||||
* posledni, kdy uz ticket existoval, a tise se zahazovaly.
|
||||
*
|
||||
* **Prazdna hodnota nikdy nemaze**, co uz na ticketu je. To je ta pojistka,
|
||||
* kvuli ktere se drive zapisovalo jen pri zalozeni: pozdejsi zprava bez
|
||||
* jmena zakaznika je bezna a smazat kvuli ni jmeno by bylo horsi nez ho
|
||||
* nedoplnit. Prepise se jen to, co odesilatel opravdu poslal.
|
||||
*/
|
||||
apply?: TicketApply;
|
||||
/** Vlastni pole, ktera se doplni i na existujici ticket. */
|
||||
fields?: Record<string, string | number | boolean | null>;
|
||||
/** Tagy, ktere se **pridaji**. Existujici se nemazou. */
|
||||
@@ -1159,10 +1198,11 @@ export function intakeEvent(input: IntakeInput): IntakeResult {
|
||||
|
||||
appendEvent(existing, event);
|
||||
|
||||
if (input.fields) existing.fields = { ...(existing.fields ?? {}), ...input.fields };
|
||||
if (input.fields) existing.fields = mergeFields(existing.fields, input.fields);
|
||||
if (input.addTags && input.addTags.length > 0) {
|
||||
existing.tags = [...new Set([...(existing.tags ?? []), ...input.addTags])];
|
||||
}
|
||||
applyValues(existing, input.apply);
|
||||
touch(existing);
|
||||
|
||||
/*
|
||||
@@ -1192,20 +1232,37 @@ export function intakeEvent(input: IntakeInput): IntakeResult {
|
||||
return { ticket: toTicket(existing), created: false, event, repeated: false };
|
||||
}
|
||||
|
||||
/*
|
||||
* Pri zalozeni plati totez co u doplneni, jen se navic dosadi zaloha tam,
|
||||
* kde odesilatel nic neposlal. Dva ruzne seznamy poli by se rozesly a zase
|
||||
* by nekde neco chybelo.
|
||||
*/
|
||||
const apply = input.apply ?? {};
|
||||
const created = createTicket({
|
||||
tenantId: input.tenantId,
|
||||
externalId: input.externalId,
|
||||
externalSource: input.externalSource ?? null,
|
||||
subject: input.create?.subject ?? event.label,
|
||||
body: input.create?.body ?? '',
|
||||
sourceRef: input.create?.sourceRef ?? null,
|
||||
channel: input.create?.channel ?? 'form',
|
||||
customer: input.create?.customer ?? { id: null, company: '', contact: '', reply: '' },
|
||||
priority: input.create?.priority ?? 'normal',
|
||||
assigneeId: input.create?.assigneeId ?? null,
|
||||
assigneeGroupId: input.create?.assigneeGroupId ?? null,
|
||||
typeId: input.create?.typeId ?? null,
|
||||
fields: { ...(input.create?.fields ?? {}), ...(input.fields ?? {}) },
|
||||
subject: apply.subject || input.create?.subject || event.label,
|
||||
/*
|
||||
* Stav uz pri vzniku. Volajici ho posilal, ale sem se 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.
|
||||
*/
|
||||
status: input.create?.status,
|
||||
body: apply.body ?? '',
|
||||
sourceRef: apply.sourceRef ?? null,
|
||||
channel: apply.channel ?? 'form',
|
||||
customer: {
|
||||
id: null,
|
||||
company: apply.company ?? '',
|
||||
contact: apply.contact ?? '',
|
||||
reply: apply.reply ?? '',
|
||||
},
|
||||
priority: apply.priority ?? 'normal',
|
||||
assigneeId: apply.assigneeId ?? null,
|
||||
assigneeGroupId: apply.assigneeGroupId ?? null,
|
||||
typeId: apply.typeId ?? null,
|
||||
fields: mergeFields(input.create?.fields, input.fields ?? {}),
|
||||
tags: [...new Set([...(input.create?.tags ?? []), ...(input.addTags ?? [])])],
|
||||
automationId: input.create?.automationId ?? null,
|
||||
trace: [
|
||||
@@ -1236,6 +1293,64 @@ export function intakeEvent(input: IntakeInput): IntakeResult {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Slouci vlastni pole typu ticketu.
|
||||
*
|
||||
* Klice se **scitaji**: kdyz prvni zprava prinese `data` a druha `data2`, ma
|
||||
* ticket obe. Odesilatel nemusi posilat vsechno pokazde a nemusi se predem
|
||||
* dohodnout, co vsechno posle.
|
||||
*
|
||||
* **Prazdny retezec nemaze.** Sablona, ktera na nic neukazuje, se dosadi
|
||||
* prazdnem, takze `{"vysledek":"{{result}}"}` u zpravy bez vysledku posle
|
||||
* prazdno - a to by prepsalo hodnotu z minule zpravy. Vymazat pole jde
|
||||
* poslanim `null`: to uz je zamer, ne vedlejsi ucinek nevyplnene sablony.
|
||||
*/
|
||||
function mergeFields(
|
||||
current: Record<string, string | number | boolean | null> | undefined,
|
||||
incoming: Record<string, string | number | boolean | null>,
|
||||
): Record<string, string | number | boolean | null> {
|
||||
const merged = { ...(current ?? {}) };
|
||||
for (const [key, value] of Object.entries(incoming)) {
|
||||
if (value === '') continue;
|
||||
merged[key] = value;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zapise na ticket to, co prinesla udalost.
|
||||
*
|
||||
* Jedno pravidlo pro vsechna pole: **neprazdna hodnota prepise, prazdna nemaze**.
|
||||
* Pozdejsi zprava o teze veci je upresneni, ne druhy zaznam - a zaroven nesmi
|
||||
* smazat to, co uz na ticketu je, jen proto, ze o tom nic nevi.
|
||||
*
|
||||
* Stav tudy zamerne nechodi. Ma svoje `updateTicketStatus`, ktere resi i priznak
|
||||
* vyrizeni, cas vyreseni a pocet znovuotevreni - obejit ho by ta cisla rozbilo.
|
||||
*/
|
||||
function applyValues(ticket: StoredTicket, apply: TicketApply | undefined): void {
|
||||
if (!apply) return;
|
||||
|
||||
if (apply.subject) ticket.subject = apply.subject;
|
||||
if (apply.body) ticket.body = apply.body;
|
||||
if (apply.typeId) ticket.typeId = apply.typeId;
|
||||
if (apply.priority) ticket.priority = apply.priority;
|
||||
if (apply.channel) ticket.channel = apply.channel;
|
||||
if (apply.sourceRef) ticket.sourceRef = apply.sourceRef;
|
||||
if (apply.assigneeGroupId) ticket.assigneeGroupId = apply.assigneeGroupId;
|
||||
|
||||
// Stejne jako pri zalozeni: mrtvy odkaz na resitele radeji nez ulozit.
|
||||
if (apply.assigneeId) {
|
||||
if (findPerson(apply.assigneeId)) ticket.assigneeId = apply.assigneeId;
|
||||
else console.warn(`[tickets] neznamy resitel ${apply.assigneeId}, ticket zustava jak byl`);
|
||||
}
|
||||
|
||||
// Po polozkach, at zprava, ktera zna jen telefon, neprepise jmeno firmy.
|
||||
const customer = ticket.customer;
|
||||
if (apply.company) customer.company = apply.company;
|
||||
if (apply.contact) customer.contact = apply.contact;
|
||||
if (apply.reply) customer.reply = apply.reply;
|
||||
}
|
||||
|
||||
/** Kolik udalosti se u jednoho ticketu drzi. Starsi se odmazavaji. */
|
||||
const MAX_EVENTS = 200;
|
||||
|
||||
|
||||
+15
-10
@@ -96,18 +96,23 @@ webhookRouter.post('/ticket/:token', (req, res) => {
|
||||
payload: { ...(input.payload ?? {}), ...(input.fields ?? {}) },
|
||||
fields: input.fields,
|
||||
addTags: input.tags,
|
||||
/*
|
||||
* Vsechno poslane se zapise i na uz existujici ticket. Druha zprava o teze
|
||||
* objednavce casto nese vic nez ta prvni a zahazovat to znamenalo, ze se
|
||||
* to nikdy neulozi. Co odesilatel neposle, se nemeni.
|
||||
*/
|
||||
apply: {
|
||||
subject: input.subject,
|
||||
body: input.body,
|
||||
typeId: typeId ?? undefined,
|
||||
priority: input.priority,
|
||||
channel: input.channel,
|
||||
company: input.customer?.company,
|
||||
contact: input.customer?.contact,
|
||||
reply: input.customer?.reply,
|
||||
},
|
||||
create: {
|
||||
subject: input.subject ?? input.label ?? input.event,
|
||||
body: input.body ?? '',
|
||||
channel: input.channel ?? 'form',
|
||||
priority: input.priority ?? 'normal',
|
||||
typeId,
|
||||
customer: {
|
||||
id: null,
|
||||
company: input.customer?.company ?? '',
|
||||
contact: input.customer?.contact ?? '',
|
||||
reply: input.customer?.reply ?? '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+39
-20
@@ -251,7 +251,30 @@ const handlers: Record<string, Handler> = {
|
||||
*/
|
||||
payload: inputs.payload ? safeJson(inputs.payload) : (context.trigger ?? {}),
|
||||
fields,
|
||||
/*
|
||||
* Vsechno vyplnene se zapise **i na uz existujici ticket**. Krok se
|
||||
* jmenuje "zalozit nebo doplnit" a data casto nechodi najednou: u hovoru
|
||||
* prvni zprava jen ohlasi cislo, obsah a hodnoceni dorazi az na konci.
|
||||
* Prazdna hodnota nic neprepise, o to se stara `applyValues`.
|
||||
*/
|
||||
apply: {
|
||||
subject,
|
||||
body: inputs.body?.trim() || undefined,
|
||||
typeId: inputs.typeId?.trim() || undefined,
|
||||
priority: priorities.includes(inputs.priority as TicketPriority)
|
||||
? (inputs.priority as TicketPriority)
|
||||
: undefined,
|
||||
channel,
|
||||
sourceRef: inputs.sourceRef?.trim() || undefined,
|
||||
company: inputs.company?.trim() || undefined,
|
||||
contact: inputs.contact?.trim() || undefined,
|
||||
reply: inputs.reply?.trim() || undefined,
|
||||
assigneeId: inputs.assigneeId?.trim() || undefined,
|
||||
assigneeGroupId: inputs.groupId?.trim() || undefined,
|
||||
},
|
||||
create: {
|
||||
// Zaloha predmetu jen pro vznik. Doplnovat ji pri kazde udalosti by
|
||||
// znamenalo prepisovat predmet externim ID i tam, kde ho nikdo nezadal.
|
||||
subject: subject || inputs.externalId || 'Bez předmětu',
|
||||
/*
|
||||
* Stav uz pri zalozeni. Bez toho ticket vznikl s vychozim "Nový"
|
||||
@@ -259,26 +282,6 @@ const handlers: Record<string, Handler> = {
|
||||
* u ticketu, ktery v nem nikdy nebyl.
|
||||
*/
|
||||
...(inputs.status?.trim() ? { status: inputs.status.trim() } : {}),
|
||||
body: inputs.body ?? '',
|
||||
typeId: inputs.typeId?.trim() || null,
|
||||
priority: priorities.includes(inputs.priority as TicketPriority)
|
||||
? (inputs.priority as TicketPriority)
|
||||
: 'normal',
|
||||
/*
|
||||
* Zakaznik a kanal se vyplnuji **jen pri zalozeni**. Prepisovat je
|
||||
* pri kazde dalsi udalosti by znamenalo, ze pozdejsi zprava s prazdnym
|
||||
* jmenem smaze to, co uz na ticketu bylo.
|
||||
*/
|
||||
...(channel ? { channel } : {}),
|
||||
...(inputs.sourceRef?.trim() ? { sourceRef: inputs.sourceRef.trim() } : {}),
|
||||
customer: {
|
||||
id: null,
|
||||
company: inputs.company?.trim() ?? '',
|
||||
contact: inputs.contact?.trim() ?? '',
|
||||
reply: inputs.reply?.trim() ?? '',
|
||||
},
|
||||
...(inputs.assigneeId?.trim() ? { assigneeId: inputs.assigneeId.trim() } : {}),
|
||||
...(inputs.groupId?.trim() ? { assigneeGroupId: inputs.groupId.trim() } : {}),
|
||||
},
|
||||
addTags: inputs.tags
|
||||
? inputs.tags.split(',').map((tag) => tag.trim()).filter(Boolean)
|
||||
@@ -311,6 +314,22 @@ const handlers: Record<string, Handler> = {
|
||||
if (closed !== undefined) note += closed ? ', vyřízený' : ', nevyřízený';
|
||||
}
|
||||
|
||||
/*
|
||||
* At je v logu videt, co udalost doplnila. Bez toho radek jen rekne
|
||||
* "doplnen TK-123" a nikdo nepozna cim - a prave o to jde, kdyz data
|
||||
* dorazi az druhou zpravou.
|
||||
*/
|
||||
if (!result.created) {
|
||||
const filled: string[] = [];
|
||||
if (inputs.body?.trim()) filled.push('obsah');
|
||||
if (inputs.subject?.trim()) filled.push('předmět');
|
||||
if (inputs.typeId?.trim()) filled.push('typ');
|
||||
if (inputs.company?.trim() || inputs.contact?.trim() || inputs.reply?.trim()) {
|
||||
filled.push('zákazník');
|
||||
}
|
||||
if (filled.length > 0) note += `, ${filled.join(', ')}`;
|
||||
}
|
||||
|
||||
/*
|
||||
* Odesilatel poslal totez co posledne. Neni to chyba, ale musi to byt videt,
|
||||
* jinak nikdo nepozna, ze proti nam neco tluce ve smycce.
|
||||
|
||||
Reference in New Issue
Block a user