diff --git a/documentation/06-tickety.md b/documentation/06-tickety.md index b46520b..8f2ec00 100644 --- a/documentation/06-tickety.md +++ b/documentation/06-tickety.md @@ -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. diff --git a/documentation/15-rejstrik-funkci.md b/documentation/15-rejstrik-funkci.md index 35e0e96..aa8a1c6 100644 --- a/documentation/15-rejstrik-funkci.md +++ b/documentation/15-rejstrik-funkci.md @@ -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í. | diff --git a/documentation/20-fronta-a-runtime.md b/documentation/20-fronta-a-runtime.md index 33cad52..850a974 100644 --- a/documentation/20-fronta-a-runtime.md +++ b/documentation/20-fronta-a-runtime.md @@ -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 diff --git a/documentation/99-zmeny.md b/documentation/99-zmeny.md index 14c10e2..d2a946d 100644 --- a/documentation/99-zmeny.md +++ b/documentation/99-zmeny.md @@ -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 diff --git a/src/data/services.ts b/src/data/services.ts index 938db51..a0b42a0 100644 --- a/src/data/services.ts +++ b/src/data/services.ts @@ -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', diff --git a/src/data/ticketStore.ts b/src/data/ticketStore.ts index e93e1ef..db9a9d0 100644 --- a/src/data/ticketStore.ts +++ b/src/data/ticketStore.ts @@ -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; - /** Pouzije se jen kdyz ticket teprve vznika. */ - create?: Partial>; + /** + * 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>; + /** + * 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; /** 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 | undefined, + incoming: Record, +): Record { + 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; diff --git a/src/routes/webhook.ts b/src/routes/webhook.ts index e8eb492..415380b 100644 --- a/src/routes/webhook.ts +++ b/src/routes/webhook.ts @@ -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 ?? '', - }, }, }); diff --git a/src/runtime/builtinSteps.ts b/src/runtime/builtinSteps.ts index 6c54853..e0288cb 100644 --- a/src/runtime/builtinSteps.ts +++ b/src/runtime/builtinSteps.ts @@ -251,7 +251,30 @@ const handlers: Record = { */ 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 = { * 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 = { 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.