Nastaveni prezije nasazeni, ukazkova data uz se nevraci
Bez databaze lezi data uvnitr containeru, takze redeploy je smaze a seed je nasype znovu. Dokud nebude Postgres, resi se to trema vecmi: Ukazkova data jen se SEED_DEMO=1. Automatizace, tickety a incidenty se uz po kazdem nasazeni nevraci. Konfigurace se nasypava dal, bez ni je portal nepouzitelny. Token webhooku z WEBHOOK_TOKEN_TEST. Driv se pri kazdem nasazeni vygeneroval novy, takze odesilatel musel prepisovat adresu ve svem kodu. Ted je token v promenne aplikace: neni v gitu a adresa se nemeni. Automatizace, ktera na instanci opravdu bezi, je v seedu. Je to provizorium, ne cil - az data prezijou nasazeni, patri zpatky do dat. Dal: - `ticket/upsert` umi vsechna pole ticketu: zakaznik, kanal, odkaz na zdroj, priorita, stitky, resitel, skupina a vlastni pole typu jako JSON. Zakaznik a kanal se vyplnuji jen pri zalozeni, aby pozdejsi udalost s prazdnym jmenem neprepsala, co uz tam je. - Kroky ve strome jdou sbalit, vychozi je sbaleno. Sbaleny krok ukazuje, co ma vyplneno, ne popis operace. - Seskupovani widgetu podle faze a dva nove widgety: tickety podle stavu a podle faze za tento mesic, obojí s proklikem na vyfiltrovany seznam. - Faze se ukazuje jako stav. Driv byl videt jen nas ctyrprvkovy ciselnik, coz u ticketu z cizi aplikace nedava smysl. Zivotni cyklus zustava vedle jako drobny text, protoze se z nej pocitaji statistiky. Overeno 18 kontrolami proti bezicimu serveru. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6abac82d5e
commit
ef3080d3f6
@@ -160,6 +160,26 @@ export const config = {
|
||||
* se nastavi `WORKER=0`.
|
||||
*/
|
||||
workerEnabled: process.env.WORKER !== '0',
|
||||
/**
|
||||
* Nasypat ukazkova data?
|
||||
*
|
||||
* Vychozi **ne**. Ukazkove automatizace, tickety a incidenty jsou dobre na
|
||||
* predvedeni, ale na instanci, kde uz nekdo pracuje, jsou to cizi zaznamy,
|
||||
* ktere se po kazdem redeployi vraceji. Konfigurace (firmy, uzivatele, role,
|
||||
* resitele, typy) se nasypava vzdycky - bez ni je portal nepouzitelny.
|
||||
*/
|
||||
seedDemo: process.env.SEED_DEMO === '1',
|
||||
/**
|
||||
* Token webhooku automatizace, ktera je v seedu.
|
||||
*
|
||||
* Bez databaze token nikde neprezije redeploy, takze by se po kazdem
|
||||
* nasazeni menila adresa a odesilatel by ji musel prepisovat. Tohle to drzi:
|
||||
* token je v promenne aplikace, ne v datech ani v gitu.
|
||||
*
|
||||
* Kdyz neni nastaveny, vygeneruje se novy a v logu se rekne, ze se adresa
|
||||
* zmenila.
|
||||
*/
|
||||
seedWebhookToken: (process.env.WEBHOOK_TOKEN_TEST ?? '').trim(),
|
||||
/**
|
||||
* Jak casto plánovač hleda, co je na case (v sekundach).
|
||||
*
|
||||
|
||||
+510
-438
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { config } from '../config.js';
|
||||
import { publish } from '../events/bus.js';
|
||||
import { defineStore } from './store/index.js';
|
||||
import { withMirror } from './store/mirror.js';
|
||||
@@ -364,456 +365,527 @@ function seed(
|
||||
});
|
||||
}
|
||||
|
||||
seed({
|
||||
name: 'Objednávka → sklad → fakturace',
|
||||
enabled: true,
|
||||
runsToday: 428,
|
||||
successRate: 99.3,
|
||||
avgDurationMs: 1_240,
|
||||
lastRunAt: minutesAgo(3),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'eshop',
|
||||
operationId: 'order-created',
|
||||
fields: [
|
||||
{ id: 'f_1', name: 'orderId', type: 'string', required: true },
|
||||
{ id: 'f_2', name: 'total', type: 'number', required: true },
|
||||
{ id: 'f_3', name: 'customerEmail', type: 'string', required: true },
|
||||
],
|
||||
},
|
||||
steps: [
|
||||
{ id: 'st_1', kind: 'action', serviceId: 'transform', operationId: 'map-fields' },
|
||||
{ id: 'st_2', kind: 'action', serviceId: 'eshop', operationId: 'update-stock' },
|
||||
{
|
||||
id: 'st_3',
|
||||
kind: 'condition',
|
||||
fieldId: 'f_2',
|
||||
operator: 'gte',
|
||||
value: '5000',
|
||||
yes: [
|
||||
{ id: 'st_4', kind: 'action', serviceId: 'idoklad', operationId: 'create-proforma' },
|
||||
{ id: 'st_5', kind: 'action', serviceId: 'email', operationId: 'send' },
|
||||
],
|
||||
no: [{ id: 'st_6', kind: 'action', serviceId: 'idoklad', operationId: 'create-invoice' }],
|
||||
},
|
||||
{ id: 'st_7', kind: 'action', serviceId: 'ppl', operationId: 'create-shipment' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
seed({
|
||||
name: 'Voicebot: příjem poptávek 24/7',
|
||||
enabled: true,
|
||||
runsToday: 137,
|
||||
successRate: 96.1,
|
||||
avgDurationMs: 74_000,
|
||||
lastRunAt: minutesAgo(11),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'voicebot',
|
||||
operationId: 'call-received',
|
||||
fields: [
|
||||
{ id: 'f_11', name: 'callerNumber', type: 'string', required: true },
|
||||
{ id: 'f_12', name: 'wantsOperator', type: 'boolean', required: false },
|
||||
],
|
||||
},
|
||||
steps: [
|
||||
{ id: 'st_11', kind: 'action', serviceId: 'voicebot', operationId: 'play-scenario' },
|
||||
{ id: 'st_12', kind: 'action', serviceId: 'transcription', operationId: 'transcribe' },
|
||||
{ id: 'st_13', kind: 'action', serviceId: 'ai-text', operationId: 'classify' },
|
||||
{
|
||||
id: 'st_14',
|
||||
kind: 'condition',
|
||||
fieldId: 'f_12',
|
||||
operator: 'isTrue',
|
||||
yes: [{ id: 'st_15', kind: 'action', serviceId: 'voicebot', operationId: 'transfer' }],
|
||||
no: [
|
||||
{ id: 'st_16', kind: 'action', serviceId: 'raynet', operationId: 'create-lead' },
|
||||
{ id: 'st_17', kind: 'action', serviceId: 'email', operationId: 'send' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
seed({
|
||||
name: 'Synchronizace CRM ↔ účetnictví',
|
||||
enabled: true,
|
||||
runsToday: 96,
|
||||
successRate: 98.9,
|
||||
avgDurationMs: 2_050,
|
||||
lastRunAt: minutesAgo(26),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'raynet',
|
||||
operationId: 'company-changed',
|
||||
fields: [{ id: 'f_21', name: 'companyId', type: 'string', required: true }],
|
||||
},
|
||||
steps: [
|
||||
{ id: 'st_21', kind: 'action', serviceId: 'transform', operationId: 'deduplicate' },
|
||||
{ id: 'st_22', kind: 'action', serviceId: 'idoklad', operationId: 'create-invoice' },
|
||||
{ id: 'st_23', kind: 'action', serviceId: 'log', operationId: 'write' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
seed({
|
||||
name: 'Noční report pro management',
|
||||
enabled: false,
|
||||
runsToday: 0,
|
||||
successRate: 100,
|
||||
avgDurationMs: 18_400,
|
||||
lastRunAt: minutesAgo(1_020),
|
||||
flow: {
|
||||
trigger: { serviceId: 'scheduler', operationId: 'interval', fields: [] },
|
||||
steps: [
|
||||
{ id: 'st_31', kind: 'action', serviceId: 'ga4', operationId: 'run-report' },
|
||||
{ id: 'st_32', kind: 'action', serviceId: 'google-ads', operationId: 'campaign-report' },
|
||||
{ id: 'st_33', kind: 'action', serviceId: 'sklik', operationId: 'campaign-report' },
|
||||
{ id: 'st_34', kind: 'action', serviceId: 'ai-text', operationId: 'generate' },
|
||||
{ id: 'st_35', kind: 'action', serviceId: 'email', operationId: 'send' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Ukazka webhooku s deklarovanymi parametry a podminkou nad cislem.
|
||||
seed({
|
||||
name: 'Webhook: hodnocení z dotazníku',
|
||||
enabled: true,
|
||||
runsToday: 61,
|
||||
successRate: 100,
|
||||
avgDurationMs: 640,
|
||||
lastRunAt: minutesAgo(18),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'webhook',
|
||||
operationId: 'received',
|
||||
webhookToken: generateWebhookToken(),
|
||||
fields: [
|
||||
{ id: 'f_41', name: 'customer', type: 'string', required: true },
|
||||
{ id: 'f_42', name: 'score', type: 'number', required: true },
|
||||
{ id: 'f_43', name: 'comment', type: 'string', required: false },
|
||||
],
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
id: 'st_41',
|
||||
kind: 'condition',
|
||||
fieldId: 'f_42',
|
||||
operator: 'gte',
|
||||
value: '15',
|
||||
yes: [{ id: 'st_42', kind: 'action', serviceId: 'raynet', operationId: 'add-activity' }],
|
||||
no: [
|
||||
{
|
||||
id: 'st_43',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
inputs: {
|
||||
subject: 'Nízké hodnocení od {{customer}}',
|
||||
body: 'Hodnocení {{score}} z dotazníku. Komentář: {{comment}}',
|
||||
company: '{{customer}}',
|
||||
priority: 'high',
|
||||
assigneeId: 'ppl_vomacka',
|
||||
},
|
||||
},
|
||||
{ id: 'st_44', kind: 'action', serviceId: 'microsoft365', operationId: 'post-teams' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
* Prijem z kanalu: jedna automatizace na kanal, zadne rozhodovani o resiteli.
|
||||
* Smerovani resi jedna spolecna automatizace nize - viz documentation/06-tickety.md.
|
||||
/**
|
||||
* Ukazkove automatizace.
|
||||
*
|
||||
* Nasypou se **jen se `SEED_DEMO=1`**. Na instanci, kde uz nekdo pracuje,
|
||||
* jsou to cizi zaznamy, ktere se po kazdem redeployi vraceji - a mazat je
|
||||
* porad dokola nikoho nebavi.
|
||||
*/
|
||||
|
||||
seed({
|
||||
name: 'WhatsApp: zpráva do ticketu',
|
||||
enabled: true,
|
||||
runsToday: 34,
|
||||
successRate: 100,
|
||||
avgDurationMs: 1_950,
|
||||
lastRunAt: minutesAgo(7),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'whatsapp',
|
||||
operationId: 'message-received',
|
||||
fields: [
|
||||
{ id: 'whatsapp.phone', name: 'phone', type: 'string', required: true },
|
||||
{ id: 'whatsapp.profileName', name: 'profileName', type: 'string', required: false },
|
||||
{ id: 'whatsapp.text', name: 'text', type: 'string', required: true },
|
||||
{ id: 'whatsapp.hasMedia', name: 'hasMedia', type: 'boolean', required: false },
|
||||
{ id: 'whatsapp.receivedAt', name: 'receivedAt', type: 'date', required: true },
|
||||
],
|
||||
},
|
||||
steps: [
|
||||
// Predvalidace: nejdriv se zeptame CRM, teprve podle odpovedi zakladame.
|
||||
{
|
||||
id: 'st_51',
|
||||
kind: 'action',
|
||||
serviceId: 'raynet',
|
||||
operationId: 'find-company',
|
||||
inputs: { phone: '{{phone}}' },
|
||||
},
|
||||
{
|
||||
id: 'st_52',
|
||||
kind: 'condition',
|
||||
// Odkaz na vystup kroku st_51, ne na parametr spoustece.
|
||||
fieldId: 'st_51.raynet.customerKnown',
|
||||
operator: 'isTrue',
|
||||
yes: [
|
||||
{
|
||||
id: 'st_53',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
inputs: {
|
||||
subject: 'WhatsApp od {{profileName}}',
|
||||
body: '{{text}}',
|
||||
company: '{{companyName}}',
|
||||
contact: '{{profileName}}',
|
||||
reply: '{{phone}}',
|
||||
priority: 'normal',
|
||||
assigneeId: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
no: [
|
||||
{
|
||||
id: 'st_54',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
inputs: {
|
||||
subject: 'WhatsApp od neznámého čísla',
|
||||
body: '{{text}}',
|
||||
contact: '{{profileName}}',
|
||||
reply: '{{phone}}',
|
||||
priority: 'normal',
|
||||
assigneeId: '',
|
||||
},
|
||||
},
|
||||
{ id: 'st_55', kind: 'action', serviceId: 'raynet', operationId: 'create-lead' },
|
||||
function seedDemoAutomations(): void {
|
||||
seed({
|
||||
name: 'Objednávka → sklad → fakturace',
|
||||
enabled: true,
|
||||
runsToday: 428,
|
||||
successRate: 99.3,
|
||||
avgDurationMs: 1_240,
|
||||
lastRunAt: minutesAgo(3),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'eshop',
|
||||
operationId: 'order-created',
|
||||
fields: [
|
||||
{ id: 'f_1', name: 'orderId', type: 'string', required: true },
|
||||
{ id: 'f_2', name: 'total', type: 'number', required: true },
|
||||
{ id: 'f_3', name: 'customerEmail', type: 'string', required: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
seed({
|
||||
name: 'Facebook: zpráva do ticketu',
|
||||
enabled: true,
|
||||
runsToday: 11,
|
||||
successRate: 100,
|
||||
avgDurationMs: 720,
|
||||
lastRunAt: minutesAgo(52),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'facebook',
|
||||
operationId: 'message-received',
|
||||
fields: [
|
||||
{ id: 'facebook.senderId', name: 'senderId', type: 'string', required: true },
|
||||
{ id: 'facebook.senderName', name: 'senderName', type: 'string', required: false },
|
||||
{ id: 'facebook.text', name: 'text', type: 'string', required: true },
|
||||
{ id: 'facebook.pageName', name: 'pageName', type: 'string', required: true },
|
||||
{ id: 'facebook.receivedAt', name: 'receivedAt', type: 'date', required: true },
|
||||
],
|
||||
},
|
||||
// Bez predvalidace. Z Messengeru nemame e-mail ani telefon, podle ceho
|
||||
// by se firma dohledala, takze zakladame rovnou a dohledani nechavame na cloveku.
|
||||
steps: [
|
||||
{
|
||||
id: 'st_61',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
inputs: {
|
||||
subject: 'Facebook od {{senderName}}',
|
||||
body: '{{text}}',
|
||||
contact: '{{senderName}}',
|
||||
reply: '{{senderId}}',
|
||||
priority: 'normal',
|
||||
assigneeId: '',
|
||||
steps: [
|
||||
{ id: 'st_1', kind: 'action', serviceId: 'transform', operationId: 'map-fields' },
|
||||
{ id: 'st_2', kind: 'action', serviceId: 'eshop', operationId: 'update-stock' },
|
||||
{
|
||||
id: 'st_3',
|
||||
kind: 'condition',
|
||||
fieldId: 'f_2',
|
||||
operator: 'gte',
|
||||
value: '5000',
|
||||
yes: [
|
||||
{ id: 'st_4', kind: 'action', serviceId: 'idoklad', operationId: 'create-proforma' },
|
||||
{ id: 'st_5', kind: 'action', serviceId: 'email', operationId: 'send' },
|
||||
],
|
||||
no: [{ id: 'st_6', kind: 'action', serviceId: 'idoklad', operationId: 'create-invoice' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
seed({
|
||||
name: 'E-mail: požadavky do ticketu',
|
||||
enabled: true,
|
||||
runsToday: 58,
|
||||
successRate: 99.1,
|
||||
avgDurationMs: 2_310,
|
||||
lastRunAt: minutesAgo(14),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'email',
|
||||
operationId: 'received',
|
||||
fields: [
|
||||
{ id: 'email.from', name: 'from', type: 'string', required: true },
|
||||
{ id: 'email.subject', name: 'subject', type: 'string', required: true },
|
||||
{ id: 'email.body', name: 'body', type: 'string', required: false },
|
||||
{ id: 'email.hasAttachment', name: 'hasAttachment', type: 'boolean', required: false },
|
||||
{ id: 'email.receivedAt', name: 'receivedAt', type: 'date', required: true },
|
||||
{ id: 'st_7', kind: 'action', serviceId: 'ppl', operationId: 'create-shipment' },
|
||||
],
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
id: 'st_65',
|
||||
kind: 'action',
|
||||
});
|
||||
|
||||
seed({
|
||||
name: 'Voicebot: příjem poptávek 24/7',
|
||||
enabled: true,
|
||||
runsToday: 137,
|
||||
successRate: 96.1,
|
||||
avgDurationMs: 74_000,
|
||||
lastRunAt: minutesAgo(11),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'voicebot',
|
||||
operationId: 'call-received',
|
||||
fields: [
|
||||
{ id: 'f_11', name: 'callerNumber', type: 'string', required: true },
|
||||
{ id: 'f_12', name: 'wantsOperator', type: 'boolean', required: false },
|
||||
],
|
||||
},
|
||||
steps: [
|
||||
{ id: 'st_11', kind: 'action', serviceId: 'voicebot', operationId: 'play-scenario' },
|
||||
{ id: 'st_12', kind: 'action', serviceId: 'transcription', operationId: 'transcribe' },
|
||||
{ id: 'st_13', kind: 'action', serviceId: 'ai-text', operationId: 'classify' },
|
||||
{
|
||||
id: 'st_14',
|
||||
kind: 'condition',
|
||||
fieldId: 'f_12',
|
||||
operator: 'isTrue',
|
||||
yes: [{ id: 'st_15', kind: 'action', serviceId: 'voicebot', operationId: 'transfer' }],
|
||||
no: [
|
||||
{ id: 'st_16', kind: 'action', serviceId: 'raynet', operationId: 'create-lead' },
|
||||
{ id: 'st_17', kind: 'action', serviceId: 'email', operationId: 'send' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
seed({
|
||||
name: 'Synchronizace CRM ↔ účetnictví',
|
||||
enabled: true,
|
||||
runsToday: 96,
|
||||
successRate: 98.9,
|
||||
avgDurationMs: 2_050,
|
||||
lastRunAt: minutesAgo(26),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'raynet',
|
||||
operationId: 'find-company',
|
||||
inputs: { email: '{{from}}' },
|
||||
operationId: 'company-changed',
|
||||
fields: [{ id: 'f_21', name: 'companyId', type: 'string', required: true }],
|
||||
},
|
||||
{
|
||||
id: 'st_66',
|
||||
kind: 'condition',
|
||||
fieldId: 'st_65.raynet.customerKnown',
|
||||
operator: 'isTrue',
|
||||
yes: [
|
||||
{
|
||||
id: 'st_67',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
inputs: {
|
||||
subject: '{{subject}}',
|
||||
body: '{{body}}',
|
||||
company: '{{companyName}}',
|
||||
contact: '{{from}}',
|
||||
reply: '{{from}}',
|
||||
priority: 'normal',
|
||||
assigneeId: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
no: [
|
||||
{
|
||||
id: 'st_68',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
inputs: {
|
||||
subject: '{{subject}}',
|
||||
body: '{{body}}',
|
||||
contact: '{{from}}',
|
||||
reply: '{{from}}',
|
||||
priority: 'low',
|
||||
assigneeId: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'st_69',
|
||||
kind: 'action',
|
||||
serviceId: 'email',
|
||||
operationId: 'send',
|
||||
inputs: {
|
||||
to: '{{from}}',
|
||||
subject: 'Přijali jsme váš požadavek',
|
||||
body: 'Dobrý den, požadavek jsme zaevidovali a ozveme se. Tým podpory.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
* Jedna spolecna automatizace nad vsemi tickety, at vznikly odkudkoliv.
|
||||
* Tohle je to misto, kde se dela logika zpracovani na miru zakaznikovi.
|
||||
*/
|
||||
seed({
|
||||
name: 'Směrování ticketů na řešitele',
|
||||
/*
|
||||
* 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,
|
||||
lastRunAt: minutesAgo(4),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'ticket',
|
||||
operationId: 'created',
|
||||
fields: [
|
||||
{ id: 'ticket.id', name: 'ticketId', type: 'string', required: true },
|
||||
{ id: 'ticket.subject', name: 'subject', type: 'string', required: true },
|
||||
{ id: 'ticket.body', name: 'body', type: 'string', required: false },
|
||||
{ id: 'ticket.channel', name: 'channel', type: 'string', required: true },
|
||||
{ id: 'ticket.company', name: 'company', type: 'string', required: false },
|
||||
{ id: 'ticket.contact', name: 'contact', type: 'string', required: false },
|
||||
{ id: 'ticket.priority', name: 'priority', type: 'string', required: true },
|
||||
{ id: 'ticket.knownCustomer', name: 'knownCustomer', type: 'boolean', required: true },
|
||||
{ id: 'ticket.assigned', name: 'assigned', type: 'boolean', required: true },
|
||||
steps: [
|
||||
{ id: 'st_21', kind: 'action', serviceId: 'transform', operationId: 'deduplicate' },
|
||||
{ id: 'st_22', kind: 'action', serviceId: 'idoklad', operationId: 'create-invoice' },
|
||||
{ id: 'st_23', kind: 'action', serviceId: 'log', operationId: 'write' },
|
||||
],
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
id: 'st_71',
|
||||
kind: 'condition',
|
||||
// Uz prirazeny ticket nepreberame, jinak bychom prepsali rucni rozhodnuti.
|
||||
fieldId: 'ticket.assigned',
|
||||
operator: 'isFalse',
|
||||
yes: [
|
||||
{
|
||||
id: 'st_72',
|
||||
kind: 'condition',
|
||||
fieldId: 'ticket.body',
|
||||
operator: 'contains',
|
||||
value: 'faktur',
|
||||
yes: [
|
||||
{
|
||||
id: 'st_73',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'assign',
|
||||
inputs: { ticketId: '{{ticketId}}', assigneeId: 'ppl_kriz' },
|
||||
},
|
||||
],
|
||||
no: [
|
||||
{
|
||||
id: 'st_74',
|
||||
kind: 'condition',
|
||||
fieldId: 'ticket.body',
|
||||
operator: 'contains',
|
||||
value: 'voicebot',
|
||||
yes: [
|
||||
{
|
||||
id: 'st_75',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'assign',
|
||||
inputs: { ticketId: '{{ticketId}}', assigneeId: 'ppl_novakova' },
|
||||
},
|
||||
],
|
||||
no: [
|
||||
{
|
||||
id: 'st_76',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'assign',
|
||||
inputs: { ticketId: '{{ticketId}}', assigneeId: 'ppl_vomacka' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
seed({
|
||||
name: 'Noční report pro management',
|
||||
enabled: false,
|
||||
runsToday: 0,
|
||||
successRate: 100,
|
||||
avgDurationMs: 18_400,
|
||||
lastRunAt: minutesAgo(1_020),
|
||||
flow: {
|
||||
trigger: { serviceId: 'scheduler', operationId: 'interval', fields: [] },
|
||||
steps: [
|
||||
{ id: 'st_31', kind: 'action', serviceId: 'ga4', operationId: 'run-report' },
|
||||
{ id: 'st_32', kind: 'action', serviceId: 'google-ads', operationId: 'campaign-report' },
|
||||
{ id: 'st_33', kind: 'action', serviceId: 'sklik', operationId: 'campaign-report' },
|
||||
{ id: 'st_34', kind: 'action', serviceId: 'ai-text', operationId: 'generate' },
|
||||
{ id: 'st_35', kind: 'action', serviceId: 'email', operationId: 'send' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Ukazka webhooku s deklarovanymi parametry a podminkou nad cislem.
|
||||
seed({
|
||||
name: 'Webhook: hodnocení z dotazníku',
|
||||
enabled: true,
|
||||
runsToday: 61,
|
||||
successRate: 100,
|
||||
avgDurationMs: 640,
|
||||
lastRunAt: minutesAgo(18),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'webhook',
|
||||
operationId: 'received',
|
||||
webhookToken: generateWebhookToken(),
|
||||
fields: [
|
||||
{ id: 'f_41', name: 'customer', type: 'string', required: true },
|
||||
{ id: 'f_42', name: 'score', type: 'number', required: true },
|
||||
{ id: 'f_43', name: 'comment', type: 'string', required: false },
|
||||
],
|
||||
no: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
steps: [
|
||||
{
|
||||
id: 'st_41',
|
||||
kind: 'condition',
|
||||
fieldId: 'f_42',
|
||||
operator: 'gte',
|
||||
value: '15',
|
||||
yes: [{ id: 'st_42', kind: 'action', serviceId: 'raynet', operationId: 'add-activity' }],
|
||||
no: [
|
||||
{
|
||||
id: 'st_43',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
inputs: {
|
||||
subject: 'Nízké hodnocení od {{customer}}',
|
||||
body: 'Hodnocení {{score}} z dotazníku. Komentář: {{comment}}',
|
||||
company: '{{customer}}',
|
||||
priority: 'high',
|
||||
assigneeId: 'ppl_vomacka',
|
||||
},
|
||||
},
|
||||
{ id: 'st_44', kind: 'action', serviceId: 'microsoft365', operationId: 'post-teams' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
* Prijem z kanalu: jedna automatizace na kanal, zadne rozhodovani o resiteli.
|
||||
* Smerovani resi jedna spolecna automatizace nize - viz documentation/06-tickety.md.
|
||||
*/
|
||||
|
||||
seed({
|
||||
name: 'WhatsApp: zpráva do ticketu',
|
||||
enabled: true,
|
||||
runsToday: 34,
|
||||
successRate: 100,
|
||||
avgDurationMs: 1_950,
|
||||
lastRunAt: minutesAgo(7),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'whatsapp',
|
||||
operationId: 'message-received',
|
||||
fields: [
|
||||
{ id: 'whatsapp.phone', name: 'phone', type: 'string', required: true },
|
||||
{ id: 'whatsapp.profileName', name: 'profileName', type: 'string', required: false },
|
||||
{ id: 'whatsapp.text', name: 'text', type: 'string', required: true },
|
||||
{ id: 'whatsapp.hasMedia', name: 'hasMedia', type: 'boolean', required: false },
|
||||
{ id: 'whatsapp.receivedAt', name: 'receivedAt', type: 'date', required: true },
|
||||
],
|
||||
},
|
||||
steps: [
|
||||
// Predvalidace: nejdriv se zeptame CRM, teprve podle odpovedi zakladame.
|
||||
{
|
||||
id: 'st_51',
|
||||
kind: 'action',
|
||||
serviceId: 'raynet',
|
||||
operationId: 'find-company',
|
||||
inputs: { phone: '{{phone}}' },
|
||||
},
|
||||
{
|
||||
id: 'st_52',
|
||||
kind: 'condition',
|
||||
// Odkaz na vystup kroku st_51, ne na parametr spoustece.
|
||||
fieldId: 'st_51.raynet.customerKnown',
|
||||
operator: 'isTrue',
|
||||
yes: [
|
||||
{
|
||||
id: 'st_53',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
inputs: {
|
||||
subject: 'WhatsApp od {{profileName}}',
|
||||
body: '{{text}}',
|
||||
company: '{{companyName}}',
|
||||
contact: '{{profileName}}',
|
||||
reply: '{{phone}}',
|
||||
priority: 'normal',
|
||||
assigneeId: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
no: [
|
||||
{
|
||||
id: 'st_54',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
inputs: {
|
||||
subject: 'WhatsApp od neznámého čísla',
|
||||
body: '{{text}}',
|
||||
contact: '{{profileName}}',
|
||||
reply: '{{phone}}',
|
||||
priority: 'normal',
|
||||
assigneeId: '',
|
||||
},
|
||||
},
|
||||
{ id: 'st_55', kind: 'action', serviceId: 'raynet', operationId: 'create-lead' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
seed({
|
||||
name: 'Facebook: zpráva do ticketu',
|
||||
enabled: true,
|
||||
runsToday: 11,
|
||||
successRate: 100,
|
||||
avgDurationMs: 720,
|
||||
lastRunAt: minutesAgo(52),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'facebook',
|
||||
operationId: 'message-received',
|
||||
fields: [
|
||||
{ id: 'facebook.senderId', name: 'senderId', type: 'string', required: true },
|
||||
{ id: 'facebook.senderName', name: 'senderName', type: 'string', required: false },
|
||||
{ id: 'facebook.text', name: 'text', type: 'string', required: true },
|
||||
{ id: 'facebook.pageName', name: 'pageName', type: 'string', required: true },
|
||||
{ id: 'facebook.receivedAt', name: 'receivedAt', type: 'date', required: true },
|
||||
],
|
||||
},
|
||||
// Bez predvalidace. Z Messengeru nemame e-mail ani telefon, podle ceho
|
||||
// by se firma dohledala, takze zakladame rovnou a dohledani nechavame na cloveku.
|
||||
steps: [
|
||||
{
|
||||
id: 'st_61',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
inputs: {
|
||||
subject: 'Facebook od {{senderName}}',
|
||||
body: '{{text}}',
|
||||
contact: '{{senderName}}',
|
||||
reply: '{{senderId}}',
|
||||
priority: 'normal',
|
||||
assigneeId: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
seed({
|
||||
name: 'E-mail: požadavky do ticketu',
|
||||
enabled: true,
|
||||
runsToday: 58,
|
||||
successRate: 99.1,
|
||||
avgDurationMs: 2_310,
|
||||
lastRunAt: minutesAgo(14),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'email',
|
||||
operationId: 'received',
|
||||
fields: [
|
||||
{ id: 'email.from', name: 'from', type: 'string', required: true },
|
||||
{ id: 'email.subject', name: 'subject', type: 'string', required: true },
|
||||
{ id: 'email.body', name: 'body', type: 'string', required: false },
|
||||
{ id: 'email.hasAttachment', name: 'hasAttachment', type: 'boolean', required: false },
|
||||
{ id: 'email.receivedAt', name: 'receivedAt', type: 'date', required: true },
|
||||
],
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
id: 'st_65',
|
||||
kind: 'action',
|
||||
serviceId: 'raynet',
|
||||
operationId: 'find-company',
|
||||
inputs: { email: '{{from}}' },
|
||||
},
|
||||
{
|
||||
id: 'st_66',
|
||||
kind: 'condition',
|
||||
fieldId: 'st_65.raynet.customerKnown',
|
||||
operator: 'isTrue',
|
||||
yes: [
|
||||
{
|
||||
id: 'st_67',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
inputs: {
|
||||
subject: '{{subject}}',
|
||||
body: '{{body}}',
|
||||
company: '{{companyName}}',
|
||||
contact: '{{from}}',
|
||||
reply: '{{from}}',
|
||||
priority: 'normal',
|
||||
assigneeId: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
no: [
|
||||
{
|
||||
id: 'st_68',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
inputs: {
|
||||
subject: '{{subject}}',
|
||||
body: '{{body}}',
|
||||
contact: '{{from}}',
|
||||
reply: '{{from}}',
|
||||
priority: 'low',
|
||||
assigneeId: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'st_69',
|
||||
kind: 'action',
|
||||
serviceId: 'email',
|
||||
operationId: 'send',
|
||||
inputs: {
|
||||
to: '{{from}}',
|
||||
subject: 'Přijali jsme váš požadavek',
|
||||
body: 'Dobrý den, požadavek jsme zaevidovali a ozveme se. Tým podpory.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
* Jedna spolecna automatizace nad vsemi tickety, at vznikly odkudkoliv.
|
||||
* Tohle je to misto, kde se dela logika zpracovani na miru zakaznikovi.
|
||||
*/
|
||||
seed({
|
||||
name: 'Směrování ticketů na řešitele',
|
||||
/*
|
||||
* 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,
|
||||
lastRunAt: minutesAgo(4),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'ticket',
|
||||
operationId: 'created',
|
||||
fields: [
|
||||
{ id: 'ticket.id', name: 'ticketId', type: 'string', required: true },
|
||||
{ id: 'ticket.subject', name: 'subject', type: 'string', required: true },
|
||||
{ id: 'ticket.body', name: 'body', type: 'string', required: false },
|
||||
{ id: 'ticket.channel', name: 'channel', type: 'string', required: true },
|
||||
{ id: 'ticket.company', name: 'company', type: 'string', required: false },
|
||||
{ id: 'ticket.contact', name: 'contact', type: 'string', required: false },
|
||||
{ id: 'ticket.priority', name: 'priority', type: 'string', required: true },
|
||||
{ id: 'ticket.knownCustomer', name: 'knownCustomer', type: 'boolean', required: true },
|
||||
{ id: 'ticket.assigned', name: 'assigned', type: 'boolean', required: true },
|
||||
],
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
id: 'st_71',
|
||||
kind: 'condition',
|
||||
// Uz prirazeny ticket nepreberame, jinak bychom prepsali rucni rozhodnuti.
|
||||
fieldId: 'ticket.assigned',
|
||||
operator: 'isFalse',
|
||||
yes: [
|
||||
{
|
||||
id: 'st_72',
|
||||
kind: 'condition',
|
||||
fieldId: 'ticket.body',
|
||||
operator: 'contains',
|
||||
value: 'faktur',
|
||||
yes: [
|
||||
{
|
||||
id: 'st_73',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'assign',
|
||||
inputs: { ticketId: '{{ticketId}}', assigneeId: 'ppl_kriz' },
|
||||
},
|
||||
],
|
||||
no: [
|
||||
{
|
||||
id: 'st_74',
|
||||
kind: 'condition',
|
||||
fieldId: 'ticket.body',
|
||||
operator: 'contains',
|
||||
value: 'voicebot',
|
||||
yes: [
|
||||
{
|
||||
id: 'st_75',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'assign',
|
||||
inputs: { ticketId: '{{ticketId}}', assigneeId: 'ppl_novakova' },
|
||||
},
|
||||
],
|
||||
no: [
|
||||
{
|
||||
id: 'st_76',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'assign',
|
||||
inputs: { ticketId: '{{ticketId}}', assigneeId: 'ppl_vomacka' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
no: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatizace, ktere na instanci opravdu bezi.
|
||||
*
|
||||
* Dokud data neprezivaji redeploy, je tohle jedine misto, kde nastaveni
|
||||
* prezije nasazeni. Az bude Postgres, patri to zpatky do dat a odsud pryc.
|
||||
*
|
||||
* Token webhooku se bere z `WEBHOOK_TOKEN_TEST`, aby se adresa po nasazeni
|
||||
* nemenila a odesilatel ji nemusel prepisovat.
|
||||
*/
|
||||
function seedRealAutomations(): void {
|
||||
seed({
|
||||
name: 'TEST',
|
||||
enabled: true,
|
||||
runsToday: 0,
|
||||
successRate: 100,
|
||||
avgDurationMs: 0,
|
||||
lastRunAt: minutesAgo(0),
|
||||
flow: {
|
||||
trigger: {
|
||||
serviceId: 'webhook',
|
||||
operationId: 'received',
|
||||
fields: [
|
||||
{ id: 'f_callsid', name: 'callSid', type: 'string', required: true },
|
||||
{ id: 'f_status', name: 'status', type: 'string', required: true },
|
||||
{ id: 'f_voicebot', name: 'voicebotId', type: 'string', required: true },
|
||||
],
|
||||
webhookToken: config.seedWebhookToken || generateWebhookToken(),
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
id: 'st_upsert',
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'upsert',
|
||||
inputs: {
|
||||
externalId: '{{callSid}}',
|
||||
body: '{{voicebotId}}',
|
||||
status: '{{status}}',
|
||||
tags: '{{voicebotId}}',
|
||||
priority: 'low',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
seedRealAutomations();
|
||||
|
||||
if (config.seedDemo) {
|
||||
seedDemoAutomations();
|
||||
} else {
|
||||
console.info('[automatizace] ukazkova data vypnuta (SEED_DEMO neni 1)');
|
||||
}
|
||||
|
||||
if (!config.seedWebhookToken) {
|
||||
console.warn(
|
||||
'[automatizace] WEBHOOK_TOKEN_TEST neni nastaveny, adresa webhooku se pri kazdem ' +
|
||||
'nasazeni zmeni. Nastavte ji jako promennou aplikace.',
|
||||
);
|
||||
}
|
||||
|
||||
function toSummary(stored: StoredAutomation): Automation {
|
||||
const { flow: _flow, createdAt: _createdAt, updatedAt: _updatedAt, ...rest } = stored;
|
||||
|
||||
@@ -22,7 +22,16 @@ import type { TicketChannel, TicketStatus } from './ticketStore.js';
|
||||
export type WidgetRender = 'stat' | 'chart' | 'list' | 'table' | 'gauge';
|
||||
|
||||
/** Podle ceho se da seskupovat. Bez toho by byl widget na kazdeho cloveka. */
|
||||
export type WidgetGroupBy = 'assignee' | 'group' | 'status' | 'type' | 'tag' | 'channel';
|
||||
export type WidgetGroupBy =
|
||||
| 'assignee'
|
||||
| 'group'
|
||||
/** Zivotni cyklus: novy, v reseni, ceka, vyreseno. */
|
||||
| 'status'
|
||||
/** Faze z workflow typu, napr. stavy hovoru od voicebota. */
|
||||
| 'stage'
|
||||
| 'type'
|
||||
| 'tag'
|
||||
| 'channel';
|
||||
|
||||
export type WidgetPeriod = 'today' | '7d' | '30d' | 'month' | 'all';
|
||||
|
||||
@@ -98,6 +107,32 @@ export function seedCustomWidgets(): CustomWidget[] {
|
||||
const base = { tenantId: 'tnt_automia', ownerId: null, createdAt: timestamp, updatedAt: timestamp };
|
||||
|
||||
return [
|
||||
{
|
||||
...base,
|
||||
id: 'cw_status_month',
|
||||
name: 'Tickety podle stavu',
|
||||
description: 'Kolik ticketů je v kterém stavu za tento měsíc.',
|
||||
render: 'table',
|
||||
source: {
|
||||
kind: 'ticketCount',
|
||||
filter: { period: 'month' },
|
||||
groupBy: 'status',
|
||||
},
|
||||
size: 'half',
|
||||
},
|
||||
{
|
||||
...base,
|
||||
id: 'cw_stage_month',
|
||||
name: 'Tickety podle fáze',
|
||||
description: 'Fáze z workflow typu, například stavy hovorů. Za tento měsíc.',
|
||||
render: 'table',
|
||||
source: {
|
||||
kind: 'ticketCount',
|
||||
filter: { period: 'month' },
|
||||
groupBy: 'stage',
|
||||
},
|
||||
size: 'half',
|
||||
},
|
||||
{
|
||||
...base,
|
||||
id: 'cw_idoklad_invoices',
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* kdo ji pouzivaji. Proto `tenantId: null`.
|
||||
*/
|
||||
|
||||
import { config } from '../config.js';
|
||||
import { publish } from '../events/bus.js';
|
||||
import { defineStore, nowIso, type TenantEntity } from './store/index.js';
|
||||
import { withMirror } from './store/mirror.js';
|
||||
@@ -51,7 +52,11 @@ function minutesAgo(minutes: number): string {
|
||||
return new Date(Date.now() - minutes * 60_000).toISOString();
|
||||
}
|
||||
|
||||
const incidents: Incident[] = [
|
||||
/**
|
||||
* Ukazkove incidenty. Se `SEED_DEMO=1` se pouziji, jinak se zacina naprazdno -
|
||||
* incident je hlaseni o vypadku a vymysleny vypadek nikoho nezajima.
|
||||
*/
|
||||
const demoIncidents: Incident[] = [
|
||||
{
|
||||
id: 'INC-231',
|
||||
tenantId: null,
|
||||
@@ -96,7 +101,9 @@ const incidents: Incident[] = [
|
||||
},
|
||||
];
|
||||
|
||||
let counter = 231;
|
||||
const incidents: Incident[] = config.seedDemo ? [...demoIncidents] : [];
|
||||
|
||||
let counter = config.seedDemo ? 231 : 0;
|
||||
|
||||
/** Tvar v ulozisti. Cas vzniku je `startedAt`, `createdAt` je kvuli rozhrani. */
|
||||
interface StoredIncident extends Incident, TenantEntity {}
|
||||
|
||||
@@ -532,6 +532,67 @@ export const services: Service[] = [
|
||||
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}}"}.',
|
||||
},
|
||||
{
|
||||
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.',
|
||||
},
|
||||
{ id: 'contact', label: 'Zákazník: kontakt', kind: 'text', required: false },
|
||||
{
|
||||
id: 'reply',
|
||||
label: 'Zákazník: kam odpovídat',
|
||||
kind: 'text',
|
||||
required: false,
|
||||
hint: 'E-mail nebo číslo, odkud to přišlo.',
|
||||
},
|
||||
{
|
||||
id: 'channel',
|
||||
label: 'Kanál',
|
||||
kind: 'choice',
|
||||
required: false,
|
||||
options: [
|
||||
{ value: 'whatsapp', label: 'WhatsApp' },
|
||||
{ value: 'facebook', label: 'Facebook Messenger' },
|
||||
{ value: 'instagram', label: 'Instagram' },
|
||||
{ value: 'email', label: 'E-mail' },
|
||||
{ value: 'voice', label: 'Hlasová linka' },
|
||||
{ value: 'form', label: 'Webový formulář' },
|
||||
{ value: 'portal', label: 'Portál' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'sourceRef',
|
||||
label: 'Odkaz na zdroj',
|
||||
kind: 'text',
|
||||
required: false,
|
||||
hint: 'ID zprávy u odesílatele, ať je dohledatelná.',
|
||||
},
|
||||
{
|
||||
id: 'assigneeId',
|
||||
label: 'Řešitel',
|
||||
kind: 'choice',
|
||||
required: false,
|
||||
options: [],
|
||||
optionsFrom: 'people',
|
||||
hint: 'Když víte rovnou, komu to patří. Jinak použijte samostatný krok.',
|
||||
},
|
||||
{
|
||||
id: 'groupId',
|
||||
label: 'Skupina',
|
||||
kind: 'choice',
|
||||
required: false,
|
||||
options: [],
|
||||
optionsFrom: 'groups',
|
||||
},
|
||||
{ id: 'event', label: 'Typ události', kind: 'text', required: false },
|
||||
{ id: 'label', label: 'Popisek do časové osy', kind: 'text', required: false },
|
||||
],
|
||||
|
||||
+294
-282
@@ -16,6 +16,7 @@
|
||||
* `data/store/index.ts`, tady se to neresi.
|
||||
*/
|
||||
|
||||
import { config } from '../config.js';
|
||||
import { publish } from '../events/bus.js';
|
||||
import { onTicketChanged } from './ticketHooks.js';
|
||||
import { notify } from './notifications.js';
|
||||
@@ -390,296 +391,307 @@ function seed(ticket: StoredTicket, trace: TraceInput[]) {
|
||||
traces.set(ticket.id, flattenTrace(trace, null, []));
|
||||
}
|
||||
|
||||
seed(
|
||||
{
|
||||
id: 'TK-4821',
|
||||
tenantId: 'tnt_automia',
|
||||
subject: 'Voicebot neodpovídá na volání po 18:00',
|
||||
body:
|
||||
'Dobrý den, po šesté hodině to nikdo nebere. Zkoušeli jsme to včera i dnes, ' +
|
||||
'linka jen vyzvání a pak to spadne. Přes den to funguje normálně.',
|
||||
sourceRef: 'cl_88213',
|
||||
channel: 'voice',
|
||||
customer: {
|
||||
id: 'crm_1042',
|
||||
company: 'Firma s.r.o.',
|
||||
contact: 'Petra Klientová',
|
||||
reply: '+420 601 118 224',
|
||||
},
|
||||
status: 'open',
|
||||
priority: 'high',
|
||||
assigneeId: 'ppl_novakova',
|
||||
automationId: 'AUT-02',
|
||||
createdAt: minutesAgo(310),
|
||||
updatedAt: minutesAgo(42),
|
||||
},
|
||||
[
|
||||
/**
|
||||
* Ukazkove tickety.
|
||||
*
|
||||
* Nasypou se **jen se `SEED_DEMO=1`**. Na instanci, kde uz chodi skutecny
|
||||
* provoz, jsou to cizi zaznamy mezi opravdovymi a po kazdem redeployi
|
||||
* se vraceji.
|
||||
*/
|
||||
function seedDemoTickets(): void {
|
||||
seed(
|
||||
{
|
||||
kind: 'trigger',
|
||||
serviceId: 'voicebot',
|
||||
operationId: 'call-received',
|
||||
label: 'Příchozí hovor na linku 800 100 200',
|
||||
status: 'ok',
|
||||
response: '{ "callId": "cl_88213", "from": "+420601118224", "durationSec": 96 }',
|
||||
durationMs: 120,
|
||||
agoMinutes: 310,
|
||||
id: 'TK-4821',
|
||||
tenantId: 'tnt_automia',
|
||||
subject: 'Voicebot neodpovídá na volání po 18:00',
|
||||
body:
|
||||
'Dobrý den, po šesté hodině to nikdo nebere. Zkoušeli jsme to včera i dnes, ' +
|
||||
'linka jen vyzvání a pak to spadne. Přes den to funguje normálně.',
|
||||
sourceRef: 'cl_88213',
|
||||
channel: 'voice',
|
||||
customer: {
|
||||
id: 'crm_1042',
|
||||
company: 'Firma s.r.o.',
|
||||
contact: 'Petra Klientová',
|
||||
reply: '+420 601 118 224',
|
||||
},
|
||||
status: 'open',
|
||||
priority: 'high',
|
||||
assigneeId: 'ppl_novakova',
|
||||
automationId: 'AUT-02',
|
||||
createdAt: minutesAgo(310),
|
||||
updatedAt: minutesAgo(42),
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'transcription',
|
||||
operationId: 'transcribe',
|
||||
label: 'Přepis nahrávky',
|
||||
status: 'ok',
|
||||
response:
|
||||
'{ "language": "cs", "confidence": 0.94, "text": "Dobrý den, po šesté hodině to nikdo nebere..." }',
|
||||
durationMs: 4_180,
|
||||
agoMinutes: 309,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ai-text',
|
||||
operationId: 'classify',
|
||||
label: 'Zařazení do kategorie',
|
||||
status: 'ok',
|
||||
response: '{ "category": "porucha", "priority": "high", "confidence": 0.88 }',
|
||||
durationMs: 910,
|
||||
agoMinutes: 309,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'raynet',
|
||||
operationId: 'upsert-contact',
|
||||
label: 'Dohledání firmy podle telefonu',
|
||||
status: 'ok',
|
||||
response: '{ "companyId": "crm_1042", "name": "Firma s.r.o.", "matchedBy": "phone" }',
|
||||
durationMs: 640,
|
||||
agoMinutes: 309,
|
||||
},
|
||||
{
|
||||
kind: 'condition',
|
||||
label: 'knownCustomer je splněno',
|
||||
status: 'ok',
|
||||
response: 'true, pokračuje větev ANO',
|
||||
agoMinutes: 309,
|
||||
children: [
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
label: 'Založení ticketu',
|
||||
status: 'ok',
|
||||
response: '{ "ticketId": "TK-4821", "priority": "high" }',
|
||||
durationMs: 85,
|
||||
agoMinutes: 309,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'assign',
|
||||
label: 'Přiřazení řešitele podle služby',
|
||||
status: 'ok',
|
||||
response: '{ "assignee": "Eva Nováková", "rule": "voicebot -> voiceboti" }',
|
||||
durationMs: 40,
|
||||
agoMinutes: 309,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'microsoft365',
|
||||
operationId: 'post-teams',
|
||||
label: 'Upozornění do Teams',
|
||||
status: 'error',
|
||||
response: 'HTTP 429 Too Many Requests, kanál "Servicedesk" překročil limit, zpráva neodešla',
|
||||
durationMs: 2_400,
|
||||
agoMinutes: 308,
|
||||
},
|
||||
{
|
||||
kind: 'note',
|
||||
label: 'Eva Nováková: Reprodukováno, chyba je v nočním režimu scénáře.',
|
||||
status: 'info',
|
||||
agoMinutes: 42,
|
||||
},
|
||||
],
|
||||
);
|
||||
[
|
||||
{
|
||||
kind: 'trigger',
|
||||
serviceId: 'voicebot',
|
||||
operationId: 'call-received',
|
||||
label: 'Příchozí hovor na linku 800 100 200',
|
||||
status: 'ok',
|
||||
response: '{ "callId": "cl_88213", "from": "+420601118224", "durationSec": 96 }',
|
||||
durationMs: 120,
|
||||
agoMinutes: 310,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'transcription',
|
||||
operationId: 'transcribe',
|
||||
label: 'Přepis nahrávky',
|
||||
status: 'ok',
|
||||
response:
|
||||
'{ "language": "cs", "confidence": 0.94, "text": "Dobrý den, po šesté hodině to nikdo nebere..." }',
|
||||
durationMs: 4_180,
|
||||
agoMinutes: 309,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ai-text',
|
||||
operationId: 'classify',
|
||||
label: 'Zařazení do kategorie',
|
||||
status: 'ok',
|
||||
response: '{ "category": "porucha", "priority": "high", "confidence": 0.88 }',
|
||||
durationMs: 910,
|
||||
agoMinutes: 309,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'raynet',
|
||||
operationId: 'upsert-contact',
|
||||
label: 'Dohledání firmy podle telefonu',
|
||||
status: 'ok',
|
||||
response: '{ "companyId": "crm_1042", "name": "Firma s.r.o.", "matchedBy": "phone" }',
|
||||
durationMs: 640,
|
||||
agoMinutes: 309,
|
||||
},
|
||||
{
|
||||
kind: 'condition',
|
||||
label: 'knownCustomer je splněno',
|
||||
status: 'ok',
|
||||
response: 'true, pokračuje větev ANO',
|
||||
agoMinutes: 309,
|
||||
children: [
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
label: 'Založení ticketu',
|
||||
status: 'ok',
|
||||
response: '{ "ticketId": "TK-4821", "priority": "high" }',
|
||||
durationMs: 85,
|
||||
agoMinutes: 309,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'assign',
|
||||
label: 'Přiřazení řešitele podle služby',
|
||||
status: 'ok',
|
||||
response: '{ "assignee": "Eva Nováková", "rule": "voicebot -> voiceboti" }',
|
||||
durationMs: 40,
|
||||
agoMinutes: 309,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'microsoft365',
|
||||
operationId: 'post-teams',
|
||||
label: 'Upozornění do Teams',
|
||||
status: 'error',
|
||||
response: 'HTTP 429 Too Many Requests, kanál "Servicedesk" překročil limit, zpráva neodešla',
|
||||
durationMs: 2_400,
|
||||
agoMinutes: 308,
|
||||
},
|
||||
{
|
||||
kind: 'note',
|
||||
label: 'Eva Nováková: Reprodukováno, chyba je v nočním režimu scénáře.',
|
||||
status: 'info',
|
||||
agoMinutes: 42,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
seed(
|
||||
{
|
||||
id: 'TK-4820',
|
||||
tenantId: 'tnt_automia',
|
||||
subject: 'Přidat pole IČO do synchronizace CRM a fakturace',
|
||||
body:
|
||||
'Zdravím, potřebovali bychom, aby se při synchronizaci přenášelo i IČO. ' +
|
||||
'Teď ho musíme do faktur doplňovat ručně a občas se na to zapomene. ' +
|
||||
'Kolik by to bylo práce?',
|
||||
sourceRef: '<9f21c4@nordis.cz>',
|
||||
channel: 'email',
|
||||
customer: {
|
||||
id: 'crm_2210',
|
||||
company: 'Nordis a.s.',
|
||||
contact: 'Tomáš Beran',
|
||||
reply: 'tomas.beran@nordis.cz',
|
||||
},
|
||||
status: 'waiting',
|
||||
priority: 'normal',
|
||||
assigneeId: 'ppl_kriz',
|
||||
automationId: 'AUT-03',
|
||||
createdAt: minutesAgo(1_180),
|
||||
updatedAt: minutesAgo(190),
|
||||
},
|
||||
[
|
||||
seed(
|
||||
{
|
||||
kind: 'trigger',
|
||||
serviceId: 'email',
|
||||
operationId: 'received',
|
||||
label: 'Přijat e-mail do schránky podpora@',
|
||||
status: 'ok',
|
||||
response: '{ "from": "tomas.beran@nordis.cz", "subject": "IČO v synchronizaci", "attachments": 0 }',
|
||||
durationMs: 60,
|
||||
agoMinutes: 1_180,
|
||||
id: 'TK-4820',
|
||||
tenantId: 'tnt_automia',
|
||||
subject: 'Přidat pole IČO do synchronizace CRM a fakturace',
|
||||
body:
|
||||
'Zdravím, potřebovali bychom, aby se při synchronizaci přenášelo i IČO. ' +
|
||||
'Teď ho musíme do faktur doplňovat ručně a občas se na to zapomene. ' +
|
||||
'Kolik by to bylo práce?',
|
||||
sourceRef: '<9f21c4@nordis.cz>',
|
||||
channel: 'email',
|
||||
customer: {
|
||||
id: 'crm_2210',
|
||||
company: 'Nordis a.s.',
|
||||
contact: 'Tomáš Beran',
|
||||
reply: 'tomas.beran@nordis.cz',
|
||||
},
|
||||
status: 'waiting',
|
||||
priority: 'normal',
|
||||
assigneeId: 'ppl_kriz',
|
||||
automationId: 'AUT-03',
|
||||
createdAt: minutesAgo(1_180),
|
||||
updatedAt: minutesAgo(190),
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ai-text',
|
||||
operationId: 'extract',
|
||||
label: 'Vytažení údajů z textu',
|
||||
status: 'ok',
|
||||
response: '{ "type": "pozadavek na zmenu", "system": "CRM + iDoklad", "urgent": false }',
|
||||
durationMs: 1_120,
|
||||
agoMinutes: 1_180,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'raynet',
|
||||
operationId: 'upsert-contact',
|
||||
label: 'Dohledání firmy podle e-mailu',
|
||||
status: 'ok',
|
||||
response: '{ "companyId": "crm_2210", "name": "Nordis a.s.", "matchedBy": "emailDomain" }',
|
||||
durationMs: 520,
|
||||
agoMinutes: 1_180,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
label: 'Založení ticketu',
|
||||
status: 'ok',
|
||||
response: '{ "ticketId": "TK-4820", "priority": "normal" }',
|
||||
durationMs: 74,
|
||||
agoMinutes: 1_179,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'email',
|
||||
operationId: 'send',
|
||||
label: 'Potvrzení zadavateli',
|
||||
status: 'ok',
|
||||
response: '{ "messageId": "<a41c@automia.cz>", "to": "tomas.beran@nordis.cz" }',
|
||||
durationMs: 380,
|
||||
agoMinutes: 1_179,
|
||||
},
|
||||
{
|
||||
kind: 'note',
|
||||
label: 'Martin Kříž: Čekáme na potvrzení rozsahu od zákazníka.',
|
||||
status: 'info',
|
||||
agoMinutes: 190,
|
||||
},
|
||||
],
|
||||
);
|
||||
[
|
||||
{
|
||||
kind: 'trigger',
|
||||
serviceId: 'email',
|
||||
operationId: 'received',
|
||||
label: 'Přijat e-mail do schránky podpora@',
|
||||
status: 'ok',
|
||||
response: '{ "from": "tomas.beran@nordis.cz", "subject": "IČO v synchronizaci", "attachments": 0 }',
|
||||
durationMs: 60,
|
||||
agoMinutes: 1_180,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ai-text',
|
||||
operationId: 'extract',
|
||||
label: 'Vytažení údajů z textu',
|
||||
status: 'ok',
|
||||
response: '{ "type": "pozadavek na zmenu", "system": "CRM + iDoklad", "urgent": false }',
|
||||
durationMs: 1_120,
|
||||
agoMinutes: 1_180,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'raynet',
|
||||
operationId: 'upsert-contact',
|
||||
label: 'Dohledání firmy podle e-mailu',
|
||||
status: 'ok',
|
||||
response: '{ "companyId": "crm_2210", "name": "Nordis a.s.", "matchedBy": "emailDomain" }',
|
||||
durationMs: 520,
|
||||
agoMinutes: 1_180,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
label: 'Založení ticketu',
|
||||
status: 'ok',
|
||||
response: '{ "ticketId": "TK-4820", "priority": "normal" }',
|
||||
durationMs: 74,
|
||||
agoMinutes: 1_179,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'email',
|
||||
operationId: 'send',
|
||||
label: 'Potvrzení zadavateli',
|
||||
status: 'ok',
|
||||
response: '{ "messageId": "<a41c@automia.cz>", "to": "tomas.beran@nordis.cz" }',
|
||||
durationMs: 380,
|
||||
agoMinutes: 1_179,
|
||||
},
|
||||
{
|
||||
kind: 'note',
|
||||
label: 'Martin Kříž: Čekáme na potvrzení rozsahu od zákazníka.',
|
||||
status: 'info',
|
||||
agoMinutes: 190,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
seed(
|
||||
{
|
||||
id: 'TK-4819',
|
||||
tenantId: 'tnt_automia',
|
||||
subject: 'Chybí denní report objednávek v e-mailu',
|
||||
body: 'Dobrý den, už třetí den nechodí ranní report. Můžete se na to prosím podívat?',
|
||||
sourceRef: 'wamid.HBgLNDIwNzc0OTAyMzMx',
|
||||
channel: 'whatsapp',
|
||||
customer: {
|
||||
id: null,
|
||||
company: 'Neznámá firma',
|
||||
contact: 'Bistro Kolektiv',
|
||||
reply: '+420 774 902 331',
|
||||
},
|
||||
status: 'new',
|
||||
priority: 'normal',
|
||||
assigneeId: null,
|
||||
automationId: 'AUT-01',
|
||||
createdAt: minutesAgo(95),
|
||||
updatedAt: minutesAgo(95),
|
||||
},
|
||||
[
|
||||
seed(
|
||||
{
|
||||
kind: 'trigger',
|
||||
serviceId: 'whatsapp',
|
||||
operationId: 'message-received',
|
||||
label: 'Přijata zpráva z WhatsApp',
|
||||
status: 'ok',
|
||||
response:
|
||||
'{ "from": "+420774902331", "profileName": "Bistro Kolektiv", "text": "Dobrý den, už třetí den nechodí ranní report." }',
|
||||
durationMs: 55,
|
||||
agoMinutes: 95,
|
||||
id: 'TK-4819',
|
||||
tenantId: 'tnt_automia',
|
||||
subject: 'Chybí denní report objednávek v e-mailu',
|
||||
body: 'Dobrý den, už třetí den nechodí ranní report. Můžete se na to prosím podívat?',
|
||||
sourceRef: 'wamid.HBgLNDIwNzc0OTAyMzMx',
|
||||
channel: 'whatsapp',
|
||||
customer: {
|
||||
id: null,
|
||||
company: 'Neznámá firma',
|
||||
contact: 'Bistro Kolektiv',
|
||||
reply: '+420 774 902 331',
|
||||
},
|
||||
status: 'new',
|
||||
priority: 'normal',
|
||||
assigneeId: null,
|
||||
automationId: 'AUT-01',
|
||||
createdAt: minutesAgo(95),
|
||||
updatedAt: minutesAgo(95),
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ai-text',
|
||||
operationId: 'classify',
|
||||
label: 'Zařazení do kategorie',
|
||||
status: 'ok',
|
||||
response: '{ "category": "vypadek reportu", "priority": "normal", "confidence": 0.79 }',
|
||||
durationMs: 870,
|
||||
agoMinutes: 95,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'raynet',
|
||||
operationId: 'upsert-contact',
|
||||
label: 'Dohledání firmy podle telefonu',
|
||||
status: 'error',
|
||||
response: '{ "matches": 0, "searchedBy": "phone", "value": "+420774902331" }',
|
||||
durationMs: 610,
|
||||
agoMinutes: 95,
|
||||
},
|
||||
{
|
||||
kind: 'condition',
|
||||
label: 'knownCustomer není splněno',
|
||||
status: 'skipped',
|
||||
response: 'false, pokračuje větev NE',
|
||||
agoMinutes: 95,
|
||||
children: [
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
label: 'Založení ticketu bez napojení na firmu',
|
||||
status: 'ok',
|
||||
response: '{ "ticketId": "TK-4819", "customerId": null }',
|
||||
durationMs: 68,
|
||||
agoMinutes: 95,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'raynet',
|
||||
operationId: 'create-lead',
|
||||
label: 'Založení obchodního případu k dohledání',
|
||||
status: 'ok',
|
||||
response: '{ "leadId": "lead_7781", "stage": "k overeni" }',
|
||||
durationMs: 940,
|
||||
agoMinutes: 95,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'assign',
|
||||
label: 'Přiřazení řešitele',
|
||||
status: 'skipped',
|
||||
response: 'Přeskočeno, bez známé firmy nelze určit garanta a ticket zůstal nepřiřazený',
|
||||
agoMinutes: 95,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
);
|
||||
[
|
||||
{
|
||||
kind: 'trigger',
|
||||
serviceId: 'whatsapp',
|
||||
operationId: 'message-received',
|
||||
label: 'Přijata zpráva z WhatsApp',
|
||||
status: 'ok',
|
||||
response:
|
||||
'{ "from": "+420774902331", "profileName": "Bistro Kolektiv", "text": "Dobrý den, už třetí den nechodí ranní report." }',
|
||||
durationMs: 55,
|
||||
agoMinutes: 95,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ai-text',
|
||||
operationId: 'classify',
|
||||
label: 'Zařazení do kategorie',
|
||||
status: 'ok',
|
||||
response: '{ "category": "vypadek reportu", "priority": "normal", "confidence": 0.79 }',
|
||||
durationMs: 870,
|
||||
agoMinutes: 95,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'raynet',
|
||||
operationId: 'upsert-contact',
|
||||
label: 'Dohledání firmy podle telefonu',
|
||||
status: 'error',
|
||||
response: '{ "matches": 0, "searchedBy": "phone", "value": "+420774902331" }',
|
||||
durationMs: 610,
|
||||
agoMinutes: 95,
|
||||
},
|
||||
{
|
||||
kind: 'condition',
|
||||
label: 'knownCustomer není splněno',
|
||||
status: 'skipped',
|
||||
response: 'false, pokračuje větev NE',
|
||||
agoMinutes: 95,
|
||||
children: [
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'create',
|
||||
label: 'Založení ticketu bez napojení na firmu',
|
||||
status: 'ok',
|
||||
response: '{ "ticketId": "TK-4819", "customerId": null }',
|
||||
durationMs: 68,
|
||||
agoMinutes: 95,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'raynet',
|
||||
operationId: 'create-lead',
|
||||
label: 'Založení obchodního případu k dohledání',
|
||||
status: 'ok',
|
||||
response: '{ "leadId": "lead_7781", "stage": "k overeni" }',
|
||||
durationMs: 940,
|
||||
agoMinutes: 95,
|
||||
},
|
||||
{
|
||||
kind: 'action',
|
||||
serviceId: 'ticket',
|
||||
operationId: 'assign',
|
||||
label: 'Přiřazení řešitele',
|
||||
status: 'skipped',
|
||||
response: 'Přeskočeno, bez známé firmy nelze určit garanta a ticket zůstal nepřiřazený',
|
||||
agoMinutes: 95,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (config.seedDemo) seedDemoTickets();
|
||||
|
||||
seed(
|
||||
{
|
||||
|
||||
@@ -475,6 +475,8 @@ function ticketFilterFrom(query: Record<string, unknown>, scope: ResolvedScope):
|
||||
if (typeof typeId === 'string' && typeId !== '') filter.typeId = typeId;
|
||||
const tag = query.tag;
|
||||
if (typeof tag === 'string' && tag !== '') filter.tag = tag;
|
||||
const stage = query.stage;
|
||||
if (typeof stage === 'string' && stage !== '') filter.stage = stage;
|
||||
const groupId = query.groupId;
|
||||
if (typeof groupId === 'string' && groupId !== '') filter.groupId = groupId;
|
||||
|
||||
|
||||
@@ -663,7 +663,9 @@ const sourceSchema = z.discriminatedUnion('kind', [
|
||||
z.object({
|
||||
kind: z.literal('ticketCount'),
|
||||
filter: z.record(z.unknown()).default({}),
|
||||
groupBy: z.enum(['assignee', 'group', 'status', 'type', 'tag', 'channel']).optional(),
|
||||
groupBy: z
|
||||
.enum(['assignee', 'group', 'status', 'stage', 'type', 'tag', 'channel'])
|
||||
.optional(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal('ticketList'),
|
||||
|
||||
@@ -179,6 +179,8 @@ function groupLabel(key: string, groupBy: WidgetGroupBy): string {
|
||||
return key === '' ? 'Bez typu' : (findTicketType(key)?.name ?? key);
|
||||
case 'status':
|
||||
return statusLabels[key] ?? key;
|
||||
case 'stage':
|
||||
return key === '' ? 'Bez fáze' : key;
|
||||
case 'channel':
|
||||
return channelLabels[key as keyof typeof channelLabels] ?? key;
|
||||
case 'tag':
|
||||
@@ -198,6 +200,8 @@ function groupKeys(ticket: Ticket, groupBy: string): string[] {
|
||||
return [ticket.typeId ?? ''];
|
||||
case 'status':
|
||||
return [ticket.status];
|
||||
case 'stage':
|
||||
return [ticket.stage ?? ''];
|
||||
case 'channel':
|
||||
return [ticket.channel];
|
||||
case 'tag':
|
||||
@@ -377,6 +381,7 @@ function ticketsHref(
|
||||
assignee: 'assignee',
|
||||
group: 'groupId',
|
||||
status: 'status',
|
||||
stage: 'stage',
|
||||
type: 'typeId',
|
||||
tag: 'tag',
|
||||
channel: 'channel',
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
setTicketType,
|
||||
updateTicketStatus,
|
||||
type Ticket,
|
||||
type TicketChannel,
|
||||
type TicketPriority,
|
||||
type TicketStatus,
|
||||
} from '../data/ticketStore.js';
|
||||
@@ -55,6 +56,33 @@ function missing(what: string): StepOutcome {
|
||||
|
||||
const statuses: TicketStatus[] = ['new', 'open', 'waiting', 'resolved'];
|
||||
const priorities: TicketPriority[] = ['low', 'normal', 'high', 'critical'];
|
||||
const channels: TicketChannel[] = [
|
||||
'whatsapp',
|
||||
'facebook',
|
||||
'instagram',
|
||||
'email',
|
||||
'voice',
|
||||
'form',
|
||||
'portal',
|
||||
];
|
||||
|
||||
/**
|
||||
* Z JSONu nechá jen hodnoty, ktere se do vlastniho pole vejdou.
|
||||
*
|
||||
* Vnoreny objekt nebo pole by v poli typu `string` skoncilo jako
|
||||
* `[object Object]`, coz je horsi nez ho zahodit a rict to v logu.
|
||||
*/
|
||||
function scalarFields(value: Record<string, unknown>): Record<string, string | number | boolean | null> {
|
||||
const result: Record<string, string | number | boolean | null> = {};
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
if (item === null || ['string', 'number', 'boolean'].includes(typeof item)) {
|
||||
result[key] = item as string | number | boolean | null;
|
||||
} else {
|
||||
console.warn(`[kroky] pole ${key} neni jednoducha hodnota, preskakuji`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registr kroku. Klic je `serviceId/operationId` z katalogu.
|
||||
@@ -71,6 +99,20 @@ const handlers: Record<string, Handler> = {
|
||||
const subject = inputs.subject?.trim();
|
||||
if (!subject && !inputs.externalId) return missing('předmět nebo externí ID');
|
||||
|
||||
/*
|
||||
* Vlastni pole typu ticketu. Zapisuji se jako JSON, protoze jejich klice
|
||||
* urcuje typ a nejde je mit jako pevny seznam vstupu kroku. Sablony uz
|
||||
* jsou dosazene, takze `{"orderNumber":"{{orderId}}"}` funguje.
|
||||
*
|
||||
* Doplni se i na uz existujici ticket - druha zprava o teze objednavce
|
||||
* casto nese vic, nez nesla prvni.
|
||||
*/
|
||||
const fields = inputs.fields ? scalarFields(safeJson(inputs.fields)) : undefined;
|
||||
|
||||
const channel = channels.includes(inputs.channel as TicketChannel)
|
||||
? (inputs.channel as TicketChannel)
|
||||
: undefined;
|
||||
|
||||
const result = intakeEvent({
|
||||
tenantId: context.tenantId,
|
||||
externalId: inputs.externalId?.trim() || null,
|
||||
@@ -78,6 +120,7 @@ const handlers: Record<string, Handler> = {
|
||||
type: inputs.event?.trim() || 'automation',
|
||||
label: inputs.label?.trim() || subject || 'Událost',
|
||||
payload: inputs.payload ? safeJson(inputs.payload) : {},
|
||||
fields,
|
||||
create: {
|
||||
subject: subject || inputs.externalId || 'Bez předmětu',
|
||||
body: inputs.body ?? '',
|
||||
@@ -85,6 +128,21 @@ const handlers: Record<string, Handler> = {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user