Krok prirazeni resiteli a ukazkova data mimo zivy provoz

Opraveno: `ticket/assign` nemel vykonnou cast, takze krok "Prirad resiteli"
vzdycky selhal hlaskou "operace nema vykonnou cast". Doplnen jako vnitrni krok
vedle prirazeni nejvolnejsimu ze skupiny a prirazeni podle externiho ID.

Ukazkova automatizace "Smerovani ticketu na resitele" je vypnuta. Zapnuta
prebirala tickety, ktere uz nekomu patrily podle skutecne automatizace
zakaznika, a prepsat rucni nebo cizi rozhodnuti je to nejhorsi, co muze
automatizace udelat. Do udaju spoustece zaroven pribylo `assigned`
a `knownCustomer`, aby slo napsat podminku "uz je prirazeny, nesahej na to".

`ticket/upsert` prijima `status`: kdyz hodnota patri mezi nase ctyri stavy,
nastavi stav, jinak se ulozi jako faze. Cizi aplikace posila svoje stavy
hovoru a nas zivotni cyklus je pevny, protoze se z nej pocitaji statistiky.
Do shrnuti kroku se napise, co se stalo.

Overeno pripadem z provozu: callSid do externiho ID, voicebotId jako stitek,
status jako stav. Ctyri zpravy o trech hovorech daly tri tickety, filtr na
stitek vratil jen hovory daneho voicebota a widget je spocital vcetne
prokliku na vyfiltrovany seznam.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-13 17:03:03 +02:00
co-authored by Claude Opus 5
parent a57eca123e
commit 6abac82d5e
5 changed files with 115 additions and 8 deletions
+8 -1
View File
@@ -729,7 +729,14 @@ seed({
*/
seed({
name: 'Směrování ticketů na řešitele',
enabled: true,
/*
* Ukazkova automatizace, ktera **meni data**, je vypnuta.
*
* Zapnuta by prebirala tickety, ktere uz nekomu patri podle skutecne
* automatizace zakaznika - ukazkova data nemaji sahat na zivy provoz.
* Kdo si ji chce vyzkouset, zapne si ji.
*/
enabled: false,
runsToday: 103,
successRate: 100,
avgDurationMs: 310,
+13 -1
View File
@@ -516,6 +516,15 @@ export const services: Service[] = [
{ value: 'critical', label: 'Kritická' },
],
},
{
id: 'status',
label: 'Stav',
kind: 'text',
required: false,
hint:
'Nový, v řešení, čeká nebo vyřešeno. Jinou hodnotu (například stav hovoru ' +
'od voicebota) uložíme jako fázi, protože to přesně je.',
},
{
id: 'stage',
label: 'Fáze',
@@ -699,7 +708,10 @@ export const services: Service[] = [
{
id: 'assign',
name: 'Přiřadit řešiteli',
description: 'Předá ticket konkrétnímu člověku. Objeví se mu mezi jeho tickety.',
description:
'Předá ticket konkrétnímu člověku. Objeví se mu mezi jeho tickety a dostane ' +
'upozornění. Když nechcete vybírat ručně, použijte Předat nejvolnějšímu ze skupiny.',
implementation: 'script',
inputs: [
{
id: 'ticketId',
+60 -6
View File
@@ -10,7 +10,7 @@
*/
import { createIncident } from '../data/incidentStore.js';
import { findGroup, findPersonByExternalId, listPeople } from '../data/people.js';
import { findGroup, findPerson, findPersonByExternalId, listPeople } from '../data/people.js';
import { findTicketType } from '../data/ticketTypes.js';
import {
assignTicket,
@@ -92,19 +92,39 @@ const handlers: Record<string, Handler> = {
});
/*
* Faze se nastavi az po zalozeni: u noveho ticketu jeste neni typ, takze
* by se nemela proti cemu overit. Prazdna hodnota nic nemeni.
* Stav a faze se nastavuji az po zalozeni: u noveho ticketu jeste neni typ,
* takze by se faze nemela proti cemu overit.
*
* `status` je shovivavy zamerne. Cizi aplikace posila svoje stavy
* (`ringing`, `completed`) a nas zivotni cyklus ma pevne ctyri hodnoty.
* Kdyz hodnota mezi ne patri, nastavi se stav; kdyz ne, ulozi se jako
* **faze**, protoze to presne je - postup uvnitr typu. Do shrnuti kroku
* se napise, co se stalo, aby to nebylo kouzlo.
*/
if (inputs.stage?.trim()) {
const wanted = inputs.status?.trim();
const asStage = inputs.stage?.trim();
let note = '';
if (wanted && statuses.includes(wanted as TicketStatus)) {
updateTicketStatus(result.ticket.id, wanted as TicketStatus, [context.tenantId]);
note = `, stav ${wanted}`;
}
const stage = asStage || (wanted && !statuses.includes(wanted as TicketStatus) ? wanted : '');
if (stage) {
const type = result.ticket.typeId ? findTicketType(result.ticket.typeId) : undefined;
setTicketStage(result.ticket.id, inputs.stage.trim(), type?.statuses ?? [], [
const changed = setTicketStage(result.ticket.id, stage, type?.statuses ?? [], [
context.tenantId,
]);
note += changed
? `, fáze ${stage}`
: `, fáze ${stage} NEULOŽENA (není ve workflow typu)`;
}
return {
ok: true,
summary: result.created ? `založen ${result.ticket.id}` : `doplněn ${result.ticket.id}`,
summary:
(result.created ? `založen ${result.ticket.id}` : `doplněn ${result.ticket.id}`) + note,
detail: null,
ticketId: result.ticket.id,
outputs: {
@@ -166,6 +186,40 @@ const handlers: Record<string, Handler> = {
};
},
/**
* Preda ticket konkretnimu cloveku.
*
* Nejjednodussi zpusob prirazeni: vim, komu to patri, a reknu to. Kdyz je
* lidi vic a nechci vybirat rucne, je na to `assign-least-busy`.
*/
'ticket/assign': (inputs, context) => {
const ticketId = inputs.ticketId?.trim() || context.ticketId;
if (!ticketId) return missing('ticket');
const assigneeId = inputs.assigneeId?.trim();
if (!assigneeId) return missing('řešitele');
const person = findPerson(assigneeId);
if (!person || person.tenantId !== context.tenantId) {
return {
ok: false,
summary: `řešitel ${assigneeId} neexistuje`,
detail: 'Zkontrolujte ID řešitele v kroku. Řešitelé se spravují v Nastavení.',
outputs: {},
};
}
const updated = assignTicket(ticketId, person.id, [context.tenantId]);
if (!updated) return { ok: false, summary: 'přiřazení se nepodařilo', outputs: {} };
return {
ok: true,
summary: person.name,
detail: null,
outputs: { assigneeId: person.id, assigneeName: person.name },
};
},
/**
* Preda ticket cloveku podle ID z cizi aplikace.
*
+7
View File
@@ -172,7 +172,14 @@ export function ticketPayload(ticket: Ticket): Record<string, unknown> {
stage: ticket.stage,
tags: ticket.tags,
assigneeId: ticket.assignee?.id ?? null,
/*
* Aby slo napsat "uz je prirazeny, nesahej na to". Bez toho podminka
* nema z ceho vychazet a automatizace prebere i ticket, ktery uz nekomu
* patri - a prepsat rucni rozhodnuti je to nejhorsi, co muze udelat.
*/
assigned: ticket.assignee !== null,
assigneeGroupId: ticket.assigneeGroupId,
knownCustomer: ticket.customer.id !== null,
company: ticket.customer.company,
contact: ticket.customer.contact,
reply: ticket.customer.reply,