import { config } from './config.js'; /** * Sprava zaznamu ma u kazde entity stejnou petici endpointu, protoze ji na * serveru dela jedna fabrika (`routes/crud.ts`). Popisovat ji devetkrat rucne * by znamenalo devet mist, ktere se casem rozejdou. */ function crudPaths(entity: { /** Cast cesty, napr. `roles`. */ path: string; /** Jak se o tom mluvi v popisu, napr. `roli`. */ label: string; /** Pravo, ktere je na zapis potreba. */ permission: string; }) { const id = { name: 'id', in: 'path', required: true, schema: { type: 'string' } }; const body = { required: true, content: { 'application/json': { schema: { type: 'object' } } }, }; const record = { description: 'Zaznam', content: { 'application/json': { schema: { type: 'object' } } }, }; const denied = { '403': { description: `Chybi pravo ${entity.permission}` } }; const base = `/api/dashboard/settings/${entity.path}`; return { [base]: { get: { tags: ['Nastaveni'], summary: `Seznam - ${entity.label}`, description: 'Vraci jen zaznamy firem, do kterych volajici patri.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Seznam', content: { 'application/json': { schema: { type: 'object', properties: { items: { type: 'array', items: { type: 'object' } } }, }, }, }, }, ...denied, }, }, post: { tags: ['Nastaveni'], summary: `Vytvorit - ${entity.label}`, security: [{ bearerAuth: [] }], requestBody: body, responses: { '201': record, '400': { description: 'Neplatny vstup' }, ...denied }, }, }, [`${base}/{id}`]: { get: { tags: ['Nastaveni'], summary: `Detail - ${entity.label}`, security: [{ bearerAuth: [] }], parameters: [id], responses: { '200': record, '404': { description: 'Neexistuje' }, ...denied }, }, patch: { tags: ['Nastaveni'], summary: `Upravit - ${entity.label}`, description: 'Posilaji se jen menena pole. ID a cas vzniku se prepsat nedaji.', security: [{ bearerAuth: [] }], parameters: [id], requestBody: body, responses: { '200': record, '400': { description: 'Neplatny vstup' }, '404': { description: 'Neexistuje' }, ...denied, }, }, delete: { tags: ['Nastaveni'], summary: `Smazat - ${entity.label}`, security: [{ bearerAuth: [] }], parameters: [id], responses: { '204': { description: 'Smazano' }, '404': { description: 'Neexistuje' }, ...denied, }, }, }, }; } /** Entity, ktere se spravuji v Nastaveni. Jeden radek na entitu. */ const settingsEntities = [ { path: 'tenants', label: 'firmy', permission: 'tenant.manage' }, { path: 'users', label: 'uzivatele', permission: 'user.manage' }, { path: 'roles', label: 'role a prava', permission: 'role.manage' }, { path: 'people', label: 'resitele', permission: 'people.manage' }, { path: 'groups', label: 'skupiny resitelu', permission: 'group.manage' }, { path: 'ticket-types', label: 'typy ticketu', permission: 'ticketType.manage' }, { path: 'actions', label: 'akce na ticketu', permission: 'action.manage' }, { path: 'widgets', label: 'vlastni widgety', permission: 'widget.manage' }, { path: 'features', label: 'zalozky firmy', permission: 'tenant.manage' }, ]; /** * OpenAPI popis API. * * `servers` MUSI obsahovat prefix reverse proxy, jinak Swagger "Try it out" * vola endpointy na root domene a dostane 404 (viz AGENTS.md). * Prefix se bere z ROOT_PATH, nikdy se nehardcoduje. */ export function buildOpenApiDocument() { const server = config.rootPath === '' ? '/' : config.rootPath; return { openapi: '3.0.3', info: { title: 'Automia - portal a API', version: '1.0.0', description: 'Webova prezentace a klientsky portal. Automatizace, voiceboti, integrace, ' + 'tickety a incidenty. Aplikace bezi za reverse proxy AppFactory.', }, servers: [{ url: server, description: 'Verejna adresa vcetne prefixu proxy' }], tags: [ { name: 'Provoz', description: 'Health a zakladni informace' }, { name: 'Autentizace', description: 'Prihlaseni do portalu' }, { name: 'Dashboard', description: 'Data klientskeho portalu' }, { name: 'Tickety', description: 'Pozadavky, jejich resitele a log prubehu' }, { name: 'Automatizace', description: 'Sprava automatizaci a stromu akci' }, { name: 'Sluzby', description: 'Katalog toho, co umime napojit' }, { name: 'Konektory', description: 'Napojeni firmy na sluzbu vcetne pristupovych udaju' }, { name: 'Skripty', description: 'Vykonna cast sluzby: manifest, kod a zkusebni beh' }, { name: 'Nastaveni', description: 'Firmy, lide, role a prava, typy ticketu, akce, widgety' }, { name: 'Sprava platformy', description: 'Audit a prepnuti na jiny ucet' }, { name: 'Webhook', description: 'Verejny prijem dat do automatizace' }, { name: 'Kontakt', description: 'Poptavkovy formular z webu' }, ], components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', description: 'Token z POST /api/auth/login. Vlozte samotny token bez slova Bearer.', }, }, schemas: { Error: { type: 'object', properties: { error: { type: 'string', example: 'validation_error' }, message: { type: 'string', example: 'Zadejte platny e-mail.' }, }, }, User: { type: 'object', description: 'Uzivatel muze patrit do vic firem. Role je vzdy az uvnitr firmy, ' + 'pristup napric firmami je zvlast jako platformAdmin.', properties: { id: { type: 'string', example: 'usr_1' }, email: { type: 'string', example: 'admin@automia.cz' }, name: { type: 'string', example: 'Jiri Uhlir' }, platformAdmin: { type: 'boolean', description: 'Vidi napric vsemi firmami a muze mezi nimi prepinat.', }, memberships: { type: 'array', items: { type: 'object', properties: { tenantId: { type: 'string', example: 'tnt_automia' }, role: { type: 'string', enum: ['admin', 'agent'] }, }, }, }, }, }, Access: { type: 'object', description: 'Co uzivatel smi. Klient podle toho kresli prepinac pohledu.', properties: { scopes: { type: 'array', items: { type: 'string', enum: ['all', 'tenant', 'mine'] }, }, tenants: { type: 'array', items: { type: 'object', properties: { id: { type: 'string', example: 'tnt_automia' }, name: { type: 'string', example: 'Automia' }, }, }, }, defaultTenantId: { type: 'string', nullable: true }, canAssignOthers: { type: 'boolean', description: 'Smi prehazovat tickety mezi lidmi, ne jen brat na sebe.', }, personId: { type: 'string', nullable: true }, }, }, Connector: { type: 'object', description: 'Napojeni firmy na jednu sluzbu. Hodnoty pristupovych udaju tady zamerne ' + 'nejsou a nikdy nebudou - secrets se z beznych endpointu nevraci.', properties: { id: { type: 'string', example: 'con_1a2b3c4d' }, tenantId: { type: 'string', example: 'tnt_automia' }, serviceId: { type: 'string', example: 'idoklad' }, name: { type: 'string', example: 'iDoklad Automia' }, baseUrl: { type: 'string', nullable: true }, enabled: { type: 'boolean' }, status: { type: 'string', enum: ['untested', 'ok', 'error'] }, lastCheckAt: { type: 'string', format: 'date-time', nullable: true }, lastError: { type: 'string', nullable: true }, checkCount: { type: 'integer', description: 'Kolik zaznamu o overeni je v historii. Samotna historie se cte pres ' + '/api/dashboard/connectors/{id}/checks - v seznamu by to byla tela odpovedi navic.', }, isDefault: { type: 'boolean', description: 'Krok stromu bez vybraneho konektoru pouzije tenhle.', }, filled: { type: 'array', items: { type: 'string' }, description: 'ID poli, ktera jsou vyplnena. Hodnoty se nevraci.', }, missing: { type: 'array', items: { type: 'string' }, description: 'ID povinnych poli, ktera chybi.', }, config: { type: 'object', additionalProperties: { type: 'string' }, description: 'Necitliva nastaveni. Tajna pole tu nejsou vubec.', }, ready: { type: 'boolean' }, }, }, ScriptField: { type: 'object', description: 'Parametr skriptu. Stejny tvar pro vstup i vystup - kontrola je pak ' + 'jedna funkce, ne dve skoro stejne.', required: ['id', 'label', 'type', 'required'], properties: { id: { type: 'string', example: 'invoiceId', description: 'Pouziva se v sablonach jako {{invoiceId}}.', }, label: { type: 'string', example: 'ID faktury v iDokladu' }, type: { type: 'string', enum: ['string', 'number', 'boolean', 'date'] }, required: { type: 'boolean' }, hint: { type: 'string' }, options: { type: 'array', description: 'Vyber z hodnot. Jina hodnota neprojde kontrolou.', items: { type: 'object', properties: { value: { type: 'string' }, label: { type: 'string' } }, }, }, pattern: { type: 'string', description: 'Jen u typu string.' }, multiline: { type: 'boolean', description: 'Jen u typu string.' }, default: { description: 'Dosadi se, kdyz hodnota chybi a parametr neni povinny.' }, }, }, ScriptManifest: { type: 'object', description: 'Co skript umi. Podle nej s nim umi pracovat strom automatizace.', properties: { id: { type: 'string', example: 'idoklad.get-issued-invoice', description: 'Tvar sluzba.operace. Nazev souboru musi byt .js.', }, serviceId: { type: 'string', example: 'idoklad' }, serviceName: { type: 'string', example: 'iDoklad' }, operationId: { type: 'string', example: 'get-issued-invoice' }, name: { type: 'string', example: 'ZĂ­skat vydanou fakturu' }, description: { type: 'string' }, inputs: { type: 'array', items: { $ref: '#/components/schemas/ScriptField' } }, outputs: { type: 'array', items: { $ref: '#/components/schemas/ScriptField' } }, timeoutMs: { type: 'integer', example: 15000 }, }, }, ScriptProblem: { type: 'object', description: 'Rozbity skript. Nesmi shodit ostatni ani tise zmizet, proto se vraci sem.', properties: { file: { type: 'string', example: 'idoklad.get-issued-invoice.js' }, scriptId: { type: 'string', nullable: true }, message: { type: 'string' }, issues: { type: 'array', items: { type: 'object', properties: { field: { type: 'string' }, message: { type: 'string' } }, }, }, }, }, ConnectionStatus: { type: 'object', description: 'Stav napojeni konektoru. Hodnoty pristupovych udaju se nevraci nikdy, ' + 'jen jmena promennych, ktere chybi.', properties: { connectorId: { type: 'string', example: 'idoklad' }, baseUrl: { type: 'string', example: 'https://services.csbot.cz/apps/idoklad' }, ready: { type: 'boolean' }, missing: { type: 'array', items: { type: 'string' }, example: ['IDOKLAD_CLIENT_SECRET'], }, headers: { type: 'array', items: { type: 'string' }, example: ['X-ClientId'] }, }, }, ScriptRunResult: { type: 'object', description: 'Vysledek behu skriptu. `retryable` rika, jestli ma smysl zkusit to znovu - ' + 'timeout ano, spatny vstup ne.', properties: { ok: { type: 'boolean' }, scriptId: { type: 'string' }, outputs: { type: 'object', additionalProperties: true, description: 'Prazdne, kdyz beh selhal.', }, logs: { type: 'array', items: { type: 'object', properties: { at: { type: 'string', format: 'date-time' }, message: { type: 'string' }, detail: { type: 'string' }, }, }, }, durationMs: { type: 'integer' }, httpCalls: { type: 'integer' }, error: { type: 'object', nullable: true, properties: { kind: { type: 'string', enum: [ 'not_found', 'config', 'validation', 'output', 'retryable', 'terminal', 'timeout', 'internal', ], }, message: { type: 'string' }, retryable: { type: 'boolean' }, status: { type: 'integer' }, detail: { type: 'string' }, issues: { type: 'array', items: { type: 'object', properties: { field: { type: 'string' }, message: { type: 'string' } }, }, }, }, }, }, }, LoginRequest: { type: 'object', required: ['email', 'password'], properties: { email: { type: 'string', format: 'email', example: 'admin@automia.cz' }, password: { type: 'string', format: 'password', example: 'demo1234' }, }, }, LoginResponse: { type: 'object', properties: { token: { type: 'string' }, user: { $ref: '#/components/schemas/User' }, }, }, Person: { type: 'object', description: 'Resitel ticketu. Nemusi mit ucet v portalu, spojka je e-mail.', properties: { id: { type: 'string', example: 'ppl_vomacka' }, name: { type: 'string', example: 'Karel Vomacka' }, email: { type: 'string', format: 'email' }, role: { type: 'string', example: 'Servicedesk' }, capacity: { type: 'integer', description: 'Kolik nevyrizenych ticketu je pro nej jeste zdrava zatez.', }, }, }, TicketCustomer: { type: 'object', properties: { id: { type: 'string', nullable: true, description: 'ID firmy v CRM. null = zakaznika se nepodarilo dohledat.', example: 'crm_1042', }, company: { type: 'string', example: 'Firma s.r.o.' }, contact: { type: 'string', example: 'Petra Klientova' }, reply: { type: 'string', description: 'Adresa nebo cislo, odkud pozadavek prisel a kam se odpovida.', }, }, }, Ticket: { type: 'object', properties: { id: { type: 'string', example: 'TK-4821' }, subject: { type: 'string' }, body: { type: 'string', description: 'Cely text pozadavku. Prazdny retezec = krok "Zalozit ticket" obsah nenaplnil.', }, sourceRef: { type: 'string', nullable: true, description: 'Odkaz na zdrojovou zpravu u poskytovatele.', example: 'wamid.HBgLNDIwNzc0OTAyMzMx', }, channel: { type: 'string', enum: ['whatsapp', 'facebook', 'instagram', 'email', 'voice', 'form', 'portal'], description: 'Odkud pozadavek prisel.', }, customer: { $ref: '#/components/schemas/TicketCustomer' }, status: { type: 'string', enum: ['new', 'open', 'waiting', 'resolved'] }, priority: { type: 'string', enum: ['low', 'normal', 'high', 'critical'] }, assignee: { type: 'object', nullable: true, description: 'Kdo ma ticket u sebe. null = ceka ve fronte.', properties: { id: { type: 'string', example: 'ppl_vomacka' }, name: { type: 'string', example: 'Karel Vomacka' }, }, }, automationId: { type: 'string', nullable: true, description: 'Automatizace, ktera ticket zalozila. null = zalozeno rucne.', }, createdAt: { type: 'string', format: 'date-time' }, updatedAt: { type: 'string', format: 'date-time' }, }, }, TicketTraceEntry: { type: 'object', description: 'Jeden radek logu ticketu. Strom se sklada pres parentId - vetev podminky ' + 'visi na zaznamu te podminky.', properties: { id: { type: 'string', example: 'tr_12' }, parentId: { type: 'string', nullable: true, description: 'null = zaznam v hlavni sekvenci.', }, kind: { type: 'string', enum: ['trigger', 'action', 'condition', 'note'] }, connectorId: { type: 'string', nullable: true, example: 'raynet' }, operationId: { type: 'string', nullable: true, example: 'upsert-contact' }, label: { type: 'string' }, status: { type: 'string', enum: ['ok', 'error', 'skipped', 'info'] }, response: { type: 'string', nullable: true, description: 'Co sluzba vratila. Kvuli tomuhle log existuje.', }, durationMs: { type: 'integer', nullable: true }, at: { type: 'string', format: 'date-time' }, }, }, TicketDetail: { allOf: [ { $ref: '#/components/schemas/Ticket' }, { type: 'object', properties: { trace: { type: 'array', items: { $ref: '#/components/schemas/TicketTraceEntry' }, }, }, }, ], }, Workload: { type: 'object', description: 'Prehled nad firmou - kdo ma kolik ticketu u sebe.', properties: { rows: { type: 'array', items: { type: 'object', properties: { person: { $ref: '#/components/schemas/Person' }, open: { type: 'integer', description: 'Nevyresene tickety.' }, total: { type: 'integer' }, critical: { type: 'integer' }, oldestOpenAt: { type: 'string', format: 'date-time', nullable: true }, overloaded: { type: 'boolean' }, }, }, }, unassigned: { type: 'integer', description: 'Nevyresene tickety bez resitele.' }, openTotal: { type: 'integer' }, }, }, Incident: { type: 'object', properties: { id: { type: 'string', example: 'INC-231' }, title: { type: 'string' }, service: { type: 'string' }, severity: { type: 'string', enum: ['sev1', 'sev2', 'sev3'] }, status: { type: 'string', enum: ['investigating', 'identified', 'monitoring', 'resolved'], }, startedAt: { type: 'string', format: 'date-time' }, resolvedAt: { type: 'string', format: 'date-time', nullable: true }, }, }, TriggerField: { type: 'object', required: ['id', 'name', 'type', 'required'], properties: { id: { type: 'string', example: 'f_42' }, name: { type: 'string', example: 'score', description: 'Klic v prichozich datech, pismena, cislice a podtrzitko.', }, type: { type: 'string', enum: ['string', 'number', 'boolean', 'date'] }, required: { type: 'boolean' }, }, }, FlowStep: { type: 'object', description: 'Krok stromu. Bud akce nad konektorem, nebo podminka se dvema vetvemi.', properties: { id: { type: 'string' }, kind: { type: 'string', enum: ['action', 'condition'] }, connectorId: { type: 'string', example: 'email' }, operationId: { type: 'string', example: 'send' }, inputs: { type: 'object', additionalProperties: { type: 'string' }, description: 'Nastaveni akce. Klic je ID pole z katalogu, hodnota je sablona - ' + '{{nazev}} se nahradi parametrem spoustece. Neznamy klic vraci 400.', example: { subject: 'Reklamace od {{profileName}}', body: '{{text}}' }, }, fieldId: { type: 'string', example: 'f_42' }, operator: { type: 'string', enum: [ 'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'contains', 'startsWith', 'isEmpty', 'isNotEmpty', 'isTrue', 'isFalse', ], }, value: { type: 'string', example: '15' }, yes: { type: 'array', items: { $ref: '#/components/schemas/FlowStep' } }, no: { type: 'array', items: { $ref: '#/components/schemas/FlowStep' } }, }, }, AutomationFlow: { type: 'object', properties: { trigger: { type: 'object', nullable: true, properties: { connectorId: { type: 'string', example: 'webhook' }, operationId: { type: 'string', example: 'received' }, fields: { type: 'array', items: { $ref: '#/components/schemas/TriggerField' }, }, webhookToken: { type: 'string', readOnly: true, description: 'Generuje vyhradne server, hodnota od klienta se ignoruje.', }, }, }, steps: { type: 'array', items: { $ref: '#/components/schemas/FlowStep' } }, }, }, Automation: { type: 'object', properties: { id: { type: 'string', example: 'AUT-01' }, name: { type: 'string' }, kind: { type: 'string', enum: ['workflow', 'voicebot', 'integrace', 'report'] }, enabled: { type: 'boolean' }, runsToday: { type: 'integer' }, runsYesterday: { type: 'integer' }, runsTotal: { type: 'integer' }, successRate: { type: 'number' }, avgDurationMs: { type: 'integer' }, lastRunAt: { type: 'string', format: 'date-time' }, stepCount: { type: 'integer' }, configured: { type: 'boolean' }, issues: { type: 'array', items: { type: 'string' }, description: 'Co chybi k zapnuti. Prazdne pole znamena hotovo.', }, }, }, AutomationDetail: { allOf: [ { $ref: '#/components/schemas/Automation' }, { type: 'object', properties: { flow: { $ref: '#/components/schemas/AutomationFlow' }, createdAt: { type: 'string', format: 'date-time' }, updatedAt: { type: 'string', format: 'date-time' }, }, }, ], }, }, }, paths: { // Petice endpointu za kazdou entitu v Nastaveni, viz `crudPaths` vyse. ...settingsEntities.reduce( (all, entity) => ({ ...all, ...crudPaths(entity) }), {} as Record, ), '/health': { get: { tags: ['Provoz'], summary: 'Health check', description: 'Vraci 200, pokud je aplikace schopna prijimat provoz.', responses: { '200': { description: 'Aplikace bezi', content: { 'application/json': { schema: { type: 'object', properties: { status: { type: 'string', example: 'ok' }, uptimeSec: { type: 'integer', example: 42 }, }, }, }, }, }, }, }, }, '/api/auth/login': { post: { tags: ['Autentizace'], summary: 'Prihlaseni', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/LoginRequest' } }, }, }, responses: { '200': { description: 'Token a udaje uzivatele', content: { 'application/json': { schema: { $ref: '#/components/schemas/LoginResponse' } }, }, }, '401': { description: 'Nespravny e-mail nebo heslo', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, }, }, }, '/api/auth/me': { get: { tags: ['Autentizace'], summary: 'Prihlaseny uzivatel', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Udaje uzivatele', content: { 'application/json': { schema: { type: 'object', properties: { user: { $ref: '#/components/schemas/User' } }, }, }, }, }, '401': { description: 'Chybi nebo neplatny token' }, }, }, }, '/api/auth/logout': { post: { tags: ['Autentizace'], summary: 'Odhlaseni', security: [{ bearerAuth: [] }], responses: { '204': { description: 'Odhlaseno' } }, }, }, '/api/dashboard/summary': { get: { tags: ['Dashboard'], summary: 'Souhrn pro prehled', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Souhrnne metriky a casova rada' } }, }, }, '/api/dashboard/widgets': { get: { tags: ['Dashboard'], summary: 'Katalog widgetu prehledu', description: 'Co jde polozit na dashboard vcetne povolenych sirek.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Widgety' } }, }, }, '/api/dashboard/layout': { get: { tags: ['Dashboard'], summary: 'Rozlozeni dashboardu', description: 'Uklada se pro dvojici uzivatel a firma. `custom: false` znamena, ' + 'ze uzivatel kouka na vychozi rozlozeni.', security: [{ bearerAuth: [] }], parameters: [ { name: 'tenantId', in: 'query', schema: { type: 'string' }, description: 'Firma. Bez ni se pouzije prvni, do ktere uzivatel patri.', }, ], responses: { '200': { description: 'Rozlozeni' }, '403': { description: 'Ucet nepatri do zadne firmy' }, '404': { description: 'Firma neexistuje, nebo do ni uzivatel nepatri' }, }, }, put: { tags: ['Dashboard'], summary: 'Ulozit rozlozeni', description: 'Overuje se proti katalogu. Neznamy widget nebo sirka vraci 400.', security: [{ bearerAuth: [] }], parameters: [{ name: 'tenantId', in: 'query', schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['items'], properties: { items: { type: 'array', items: { type: 'object', required: ['id', 'widgetId', 'size'], properties: { id: { type: 'string', example: 'w1' }, widgetId: { type: 'string', example: 'stat.openTickets' }, size: { type: 'string', enum: ['third', 'half', 'full'] }, }, }, }, }, }, }, }, }, responses: { '200': { description: 'Ulozeno' }, '400': { description: 'Neplatne rozlozeni' }, }, }, delete: { tags: ['Dashboard'], summary: 'Vratit na vychozi rozlozeni', security: [{ bearerAuth: [] }], parameters: [{ name: 'tenantId', in: 'query', schema: { type: 'string' } }], responses: { '200': { description: 'Vychozi rozlozeni' } }, }, }, '/api/dashboard/access': { get: { tags: ['Dashboard'], summary: 'Co uzivatel smi videt', description: 'Povolene pohledy, firmy k prepinani a prava. Klient si to nesmi dovozovat sam.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Opravneni', content: { 'application/json': { schema: { $ref: '#/components/schemas/Access' } }, }, }, }, }, }, '/api/dashboard/people': { get: { tags: ['Tickety'], summary: 'Seznam resitelu', description: 'Lide, na ktere jde ticket priradit. `meId` je resitel odpovidajici ' + 'prihlasenemu uzivateli, nebo null, pokud zadny neni.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Resitele', content: { 'application/json': { schema: { type: 'object', properties: { items: { type: 'array', items: { $ref: '#/components/schemas/Person' } }, meId: { type: 'string', nullable: true }, }, }, }, }, }, }, }, }, '/api/dashboard/tickets': { get: { tags: ['Tickety'], summary: 'Seznam ticketu', description: 'Neznama hodnota filtru se ignoruje a zaloguje, seznam se nezuzi.', security: [{ bearerAuth: [] }], parameters: [ { name: 'scope', in: 'query', schema: { type: 'string', enum: ['all', 'tenant', 'mine'] }, description: '`all` napric firmami (jen platformni admin), `tenant` cela firma, ' + '`mine` jen moje tickety. Nepovoleny pohled vraci 403, nikdy se tise nezuzi.', }, { name: 'tenantId', in: 'query', schema: { type: 'string' }, description: 'Firma u pohledu `tenant` a `mine`. Bez clenstvi vraci 404.', example: 'tnt_automia', }, { name: 'assignee', in: 'query', schema: { type: 'string' }, description: 'ID resitele nebo `unassigned` pro frontu. U pohledu `mine` se ignoruje.', }, { name: 'status', in: 'query', schema: { type: 'string', enum: ['new', 'open', 'waiting', 'resolved'] }, }, { name: 'channel', in: 'query', schema: { type: 'string', enum: ['whatsapp', 'email', 'voice', 'form', 'portal'] }, }, ], responses: { '200': { description: 'Tickety', content: { 'application/json': { schema: { type: 'object', properties: { items: { type: 'array', items: { $ref: '#/components/schemas/Ticket' } }, meId: { type: 'string', nullable: true }, }, }, }, }, }, }, }, }, '/api/dashboard/tickets/workload': { get: { tags: ['Tickety'], summary: 'Kdo co ma u sebe', description: 'Prehled zateze pres cely tym vcetne poctu ticketu ve fronte.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Vytizeni resitelu', content: { 'application/json': { schema: { $ref: '#/components/schemas/Workload' } }, }, }, }, }, }, '/api/dashboard/tickets/{id}': { get: { tags: ['Tickety'], summary: 'Detail ticketu vcetne logu', description: 'Log obsahuje i to, co ktera volana sluzba vratila.', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' }, example: 'TK-4821', }, ], responses: { '200': { description: 'Detail', content: { 'application/json': { schema: { $ref: '#/components/schemas/TicketDetail' } }, }, }, '404': { description: 'Neexistuje' }, }, }, }, '/api/dashboard/tickets/{id}/assign': { post: { tags: ['Tickety'], summary: 'Priradit resitele', description: 'Poslete null pro vraceni ticketu do fronty.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['assigneeId'], properties: { assigneeId: { type: 'string', nullable: true, example: 'ppl_vomacka' }, }, }, }, }, }, responses: { '200': { description: 'Prirazeno', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } }, }, }, '400': { description: 'Chybi assigneeId' }, '404': { description: 'Ticket nebo resitel neexistuje' }, }, }, }, '/api/dashboard/tickets/{id}/status': { post: { tags: ['Tickety'], summary: 'Zmenit stav ticketu', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['status'], properties: { status: { type: 'string', enum: ['new', 'open', 'waiting', 'resolved'] }, }, }, }, }, }, responses: { '200': { description: 'Zmeneno', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } }, }, }, '400': { description: 'Neplatny stav' }, '404': { description: 'Neexistuje' }, }, }, }, '/api/dashboard/tickets/{id}/comment': { post: { tags: ['Tickety'], summary: 'Pridat komentar', description: 'Komentar je dalsi radek logu, aby bylo vse na jedne casove ose.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['text'], properties: { text: { type: 'string', minLength: 2 } }, }, }, }, }, responses: { '200': { description: 'Zapsano', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } }, }, }, '400': { description: 'Prazdny komentar' }, '404': { description: 'Neexistuje' }, }, }, }, '/api/dashboard/tickets/{id}/type': { post: { tags: ['Tickety'], summary: 'Nastavit typ ticketu', description: 'Typ rozhoduje, ktera vlastni pole ticket ma a ktere akce se na nem ukazou. ' + 'Pri zmene typu se hodnoty poli **nemazou**, jen prestanou byt videt.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['typeId'], properties: { typeId: { type: 'string', nullable: true, example: 'tt_order' }, fields: { type: 'object', description: 'Hodnoty vlastnich poli. Klic je klic pole z typu ticketu.', additionalProperties: true, }, }, }, }, }, }, responses: { '200': { description: 'Ulozeno', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } }, }, }, '403': { description: 'Chybi pravo ticket.type.change' }, '404': { description: 'Ticket nebo typ neexistuje' }, }, }, }, '/api/dashboard/tickets/{id}/tags': { post: { tags: ['Tickety'], summary: 'Nastavit tagy', description: 'Tagy se prepisuji cele. Prirustkova zmena by u vic lidi naraz kolidovala.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['tags'], properties: { tags: { type: 'array', maxItems: 20, items: { type: 'string', maxLength: 40 } }, }, }, }, }, }, responses: { '200': { description: 'Ulozeno', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } }, }, }, '403': { description: 'Chybi pravo ticket.tag' }, '404': { description: 'Neexistuje' }, }, }, }, '/api/dashboard/tickets/{id}/group': { post: { tags: ['Tickety'], summary: 'Prehodit na skupinu resitelu', description: 'Prirazeni konkretnimu cloveku se **zrusi**. Kdyby zustalo, ticket by byl ' + 've fronte skupiny i u cloveka a nikdo by nevedel, kdo to resi.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['groupId'], properties: { groupId: { type: 'string', nullable: true } }, }, }, }, }, responses: { '200': { description: 'Ulozeno', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } }, }, }, '403': { description: 'Chybi pravo ticket.assign.group' }, '404': { description: 'Neexistuje' }, }, }, }, '/api/dashboard/tickets/{id}/actions': { get: { tags: ['Tickety'], summary: 'Akce dostupne k ticketu', description: 'Vraci **jen akce, ktere v teto situaci opravdu jdou spustit**: sedi typ nebo ' + 'tag, projdou podminky a volajici na ne ma pravo. Klient nefiltruje nic.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': { description: 'Akce', content: { 'application/json': { schema: { type: 'object', properties: { items: { type: 'array', items: { type: 'object', properties: { id: { type: 'string' }, label: { type: 'string', example: 'Odeslat do iDokladu' }, icon: { type: 'string' }, style: { type: 'string', enum: ['primary', 'default', 'danger'] }, confirm: { type: 'string', nullable: true }, form: { type: 'array', items: { type: 'object' } }, }, }, }, }, }, }, }, }, '404': { description: 'Ticket neexistuje' }, }, }, }, '/api/dashboard/tickets/{id}/actions/{actionId}': { post: { tags: ['Tickety'], summary: 'Spustit akci', description: 'Vraci 200 **i kdyz akce selhala** - selhani akce neni chyba API. Cely prubeh ' + 'vcetne toho, co sluzba vratila, se zapise do logu ticketu.', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, { name: 'actionId', in: 'path', required: true, schema: { type: 'string' } }, ], requestBody: { required: false, content: { 'application/json': { schema: { type: 'object', properties: { form: { type: 'object', description: 'Hodnoty poli, ktera si akce vyzada.', additionalProperties: { type: 'string' }, }, }, }, }, }, }, responses: { '200': { description: 'Akce probehla nebo selhala, viz ok', content: { 'application/json': { schema: { type: 'object', properties: { ok: { type: 'boolean' }, summary: { type: 'string' }, detail: { type: 'string', nullable: true, description: 'Cele chybove hlaseni. Nikdy se nezkracuje.', }, durationMs: { type: 'integer' }, }, }, }, }, }, '403': { description: 'Chybi pravo na tuto akci' }, '404': { description: 'Ticket nebo akce neexistuje' }, }, }, }, '/api/dashboard/widget-data': { post: { tags: ['Dashboard'], summary: 'Data vlastnich widgetu', description: 'Jeden request na cely prehled. Deset dlazdic nesmi znamenat deset dotazu.', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['widgetIds'], properties: { widgetIds: { type: 'array', items: { type: 'string' } } }, }, }, }, }, responses: { '200': { description: 'Data po widgetech', content: { 'application/json': { schema: { type: 'object' } } }, }, }, }, }, '/api/dashboard/widget-data/options': { get: { tags: ['Dashboard'], summary: 'Co jde ve vlastnim widgetu nastavit', description: 'Zdroje dat, mozna seskupeni a sirky. Aby to klient nemel v kodu.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Moznosti', content: { 'application/json': { schema: { type: 'object' } } }, }, }, }, }, '/api/dashboard/settings/catalog': { get: { tags: ['Nastaveni'], summary: 'Katalog prav a modulu', description: 'Seznam vsech prav a zalozek. Formular role tak nema seznam prav v kodu klienta.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Katalog', content: { 'application/json': { schema: { type: 'object' } } }, }, }, }, }, '/api/dashboard/settings/users/{id}/password': { patch: { tags: ['Nastaveni'], summary: 'Zmenit heslo', description: 'Svoje heslo si zmeni kazdy, cizi jen spravce platformy. Hash se nikdy nevraci.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['password'], properties: { password: { type: 'string', minLength: 8 } }, }, }, }, }, responses: { '204': { description: 'Zmeneno' }, '400': { description: 'Kratke heslo' }, '403': { description: 'Cizi heslo bez prava' }, }, }, }, '/api/admin/impersonate': { post: { tags: ['Sprava platformy'], summary: 'Prepnout se na jiny ucet', description: 'Vraci novy token s narokem `act`. Bez `writes` projde **jen GET**, cokoliv ' + 'jineho vrati 403. Prepnuti i jeho ukonceni je v auditu.', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['userId'], properties: { userId: { type: 'string' }, allowWrites: { type: 'boolean', default: false, description: 'true = i zapis. Musi se zapnout vedome.', }, }, }, }, }, }, responses: { '200': { description: 'Token na cizi ucet', content: { 'application/json': { schema: { type: 'object' } } }, }, '403': { description: 'Neni spravce platformy' }, '404': { description: 'Ucet neexistuje' }, }, }, }, '/api/admin/impersonate/stop': { post: { tags: ['Sprava platformy'], summary: 'Ukoncit prepnuti', description: 'Jen zaznam do auditu. Svuj puvodni token si drzi klient, server o nem nevi.', security: [{ bearerAuth: [] }], responses: { '204': { description: 'Zapsano' } }, }, }, '/api/admin/impersonate/candidates': { get: { tags: ['Sprava platformy'], summary: 'Koho lze prepnout', description: 'Spravci platformy se nenabizeji.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Ucty', content: { 'application/json': { schema: { type: 'object' } } }, }, '403': { description: 'Neni spravce platformy' }, }, }, }, '/api/admin/audit': { get: { tags: ['Sprava platformy'], summary: 'Audit', description: 'Kdo co udelal, vcetne odepreni a vcetne toho, kdo se za koho vydaval. ' + 'Nejnovejsi nahore.', security: [{ bearerAuth: [] }], parameters: [ { name: 'action', in: 'query', schema: { type: 'string' } }, { name: 'result', in: 'query', schema: { type: 'string', enum: ['ok', 'denied'] } }, { name: 'limit', in: 'query', schema: { type: 'integer', maximum: 500, default: 200 } }, ], responses: { '200': { description: 'Zaznamy', content: { 'application/json': { schema: { type: 'object' } } }, }, '403': { description: 'Neni spravce platformy' }, }, }, }, '/api/dashboard/incidents': { get: { tags: ['Dashboard'], summary: 'Seznam incidentu', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Incidenty', content: { 'application/json': { schema: { type: 'object', properties: { items: { type: 'array', items: { $ref: '#/components/schemas/Incident' } }, }, }, }, }, }, }, }, }, '/api/dashboard/stream': { get: { tags: ['Dashboard'], summary: 'Zivy stream zmen (SSE)', description: 'Server-Sent Events. Drzi otevrene spojeni a posila udalosti, jakmile nastanou. ' + 'Swagger UI streamovanou odpoved nezobrazi rozumne, testujte prohlizecem nebo curl.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Proud udalosti text/event-stream' } }, }, }, '/health/ready': { get: { tags: ['Provoz'], summary: 'Readiness vcetne databaze', description: 'Vraci 503, kdyz je databaze nastavena a nedostupna. `/health` na databazi ' + 'zamerne nezavisi - kratky vypadek DB by jinak vedl k restartovani containeru.', responses: { '200': { description: 'Aplikace je pripravena' }, '503': { description: 'Databaze je nastavena, ale nedostupna' }, }, }, }, '/api/dashboard/storage': { get: { tags: ['Dashboard'], summary: 'Kam se uklada', description: 'mode postgres nebo memory. `ephemeral: true` znamena, ze restart procesu ' + 'data smaze. Portal to musi umet rict nahlas.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Rezim uloziste', content: { 'application/json': { schema: { type: 'object', properties: { mode: { type: 'string', enum: ['postgres', 'memory'] }, reason: { type: 'string', nullable: true }, ephemeral: { type: 'boolean' }, }, }, }, }, }, }, }, }, '/api/dashboard/services': { get: { tags: ['Sluzby'], summary: 'Katalog sluzeb pro builder', description: 'Vraci jen sluzby, ktere uzivatel vidi. Neviditelna sluzba v odpovedi neni ' + 'vubec, ne se stavem "nemate pravo". Operace, ktere obsluhuje skript, nesou ' + 'implementation: script a maji skutecne inputs a outputFields.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Sluzby, kategorie a operatory podminek' } }, }, }, '/api/dashboard/connectors/services': { get: { tags: ['Sluzby'], summary: 'Katalog sluzeb ocima firmy', description: 'Jako /services, navic connectorCount, tedy kolik konektoru na sluzbu firma ma. ' + 'Podle toho se rozlisi napojeno od muzete si napojit. Neni to vlastnost sluzby, ' + 'ale te firmy.', security: [{ bearerAuth: [] }], parameters: [{ name: 'tenantId', in: 'query', schema: { type: 'string' } }], responses: { '200': { description: 'Sluzby vcetne pouziti ve firme' } }, }, }, '/api/dashboard/connectors': { get: { tags: ['Konektory'], summary: 'Konektory firmy', description: 'Hodnoty pristupovych udaju se NIKDY nevraci, jen filled (co je vyplnene) ' + 'a missing (ktera povinna pole chybi).', security: [{ bearerAuth: [] }], parameters: [ { name: 'tenantId', in: 'query', schema: { type: 'string' } }, { name: 'serviceId', in: 'query', schema: { type: 'string' } }, ], responses: { '200': { description: 'Konektory firmy', content: { 'application/json': { schema: { type: 'object', properties: { items: { type: 'array', items: { $ref: '#/components/schemas/Connector' }, }, tenantId: { type: 'string' }, }, }, }, }, }, }, }, post: { tags: ['Konektory'], summary: 'Zalozit konektor', description: 'Pristupove udaje se posilaji ve values s klici podle Service.credentials. ' + 'Nevyplnene povinne pole neni chyba, konektor se ulozi a jen nepujde pouzit.', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['serviceId', 'name'], properties: { serviceId: { type: 'string', example: 'idoklad' }, name: { type: 'string', example: 'iDoklad Celo' }, baseUrl: { type: 'string', nullable: true }, values: { type: 'object', additionalProperties: { type: 'string' }, }, }, }, }, }, }, responses: { '201': { description: 'Zalozeno', content: { 'application/json': { schema: { $ref: '#/components/schemas/Connector' } }, }, }, '400': { description: 'Obecna sluzba konektor nepotrebuje, nebo nezname pole' }, '404': { description: 'Sluzba neexistuje nebo ji uzivatel nevidi' }, }, }, }, '/api/dashboard/connectors/{id}': { get: { tags: ['Konektory'], summary: 'Detail konektoru', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': { description: 'Konektor' }, '404': { description: 'Neexistuje' } }, }, patch: { tags: ['Konektory'], summary: 'Upravit konektor', description: 'Ve values staci poslat jen to, co se meni. PRAZDNY RETEZEC hodnotu smaze, ' + 'chybejici klic ji nechava - diky tomu jde ulozit formular, ktery tajne hodnoty ' + 'neposila. Zmena udaju vzdy zrusi predchozi overeni.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { name: { type: 'string' }, baseUrl: { type: 'string', nullable: true }, values: { type: 'object', additionalProperties: { type: 'string' } }, enabled: { type: 'boolean' }, isDefault: { type: 'boolean', enum: [true] }, }, }, }, }, }, responses: { '200': { description: 'Upraveno' }, '404': { description: 'Neexistuje' } }, }, delete: { tags: ['Konektory'], summary: 'Smazat konektor', description: 'Kdyz zmizel vychozi konektor, prevezme to prvni zbyly - jinak by kroky bez ' + 'vybraneho konektoru prestaly fungovat.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], responses: { '204': { description: 'Smazano' }, '404': { description: 'Neexistuje' } }, }, }, '/api/dashboard/connectors/{id}/test': { post: { tags: ['Konektory'], summary: 'Overit napojeni', description: 'Zavola verifyPath sluzby, coz je zamerne cteci volani vyzadujici autorizaci. ' + 'Kdyz ho sluzba nema, overi se jen /health a odpoved to v checked rekne - aby ' + 'si nikdo nemyslel, ze jsou overene i pristupove udaje. Neuspesne overeni neni ' + 'chyba API, vraci se 200 s ok: false.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': { description: 'Vysledek overeni', content: { 'application/json': { schema: { type: 'object', properties: { ok: { type: 'boolean' }, checked: { type: 'string' }, status: { type: 'integer' }, message: { type: 'string' }, baseUrl: { type: 'string', description: 'Kam konektor miri. Vraci se i pri uspechu - zaklad adresy je ' + 'z konfigurace a konektor ho smi prepsat, takze bez nej nerika ' + 'kod odpovedi nic o tom, jestli se to trefilo na spravny stroj.', }, }, }, }, }, }, '404': { description: 'Konektor neexistuje' }, }, }, }, '/api/dashboard/connectors/{id}/checks': { get: { tags: ['Konektory'], summary: 'Historie overeni konektoru', description: 'Poslednich pet overeni, nejnovejsi prvni. U neuspechu nese zaznam cele telo ' + 'odpovedi sluzby v poli detail - prave tam sluzba pise, co ji vadilo, a bez ' + 'toho se neda rozlisit spatny udaj od zakazane IP adresy. Texty jsou uz ' + 'zredigovane, pristupovy udaj v nich neni. Historie je zvlast a ne v seznamu ' + 'konektoru proto, ze telo odpovedi byva o rady velikosti vetsi nez zbytek radku.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': { description: 'Zaznamy o overeni', content: { 'application/json': { schema: { type: 'object', properties: { limit: { type: 'integer' }, items: { type: 'array', items: { type: 'object', properties: { at: { type: 'string', format: 'date-time' }, ok: { type: 'boolean' }, checked: { type: 'string' }, status: { type: 'integer', nullable: true }, message: { type: 'string' }, detail: { type: 'string', nullable: true }, request: { type: 'object', nullable: true, description: 'url je cela adresa vcetne serveru, bez query - v query muze byt tajemstvi.', properties: { method: { type: 'string' }, path: { type: 'string' }, url: { type: 'string' }, }, }, }, }, }, }, }, }, }, }, '404': { description: 'Konektor neexistuje' }, }, }, }, '/api/dashboard/scripts': { get: { tags: ['Skripty'], summary: 'Seznam skriptu konektoru', description: 'Manifesty vsech nactenych skriptu, rozbite skripty v `problems` ' + 'a stav napojeni v `connections`. Pristupove udaje se nikdy nevraci, ' + 'jen jmena chybejicich environment variables.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Skripty, problemy a stav napojeni', content: { 'application/json': { schema: { type: 'object', properties: { items: { type: 'array', items: { $ref: '#/components/schemas/ScriptManifest' }, }, problems: { type: 'array', items: { $ref: '#/components/schemas/ScriptProblem' }, }, connections: { type: 'array', items: { $ref: '#/components/schemas/ConnectionStatus' }, }, directory: { type: 'string', example: '/app/scripts' }, }, }, }, }, }, }, }, }, '/api/dashboard/scripts/reload': { post: { tags: ['Skripty'], summary: 'Znovu nacist skripty ze slozky', description: 'Skripty se nacitaji samy podle casu zmeny souboru. Tenhle endpoint ' + 'to jen vynuti hned, bez cekani.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Skripty po nacteni' }, '403': { description: 'Jen spravce platformy' }, }, }, }, '/api/dashboard/scripts/{id}': { get: { tags: ['Skripty'], summary: 'Manifest a kod skriptu', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' }, example: 'idoklad.get-issued-invoice', }, ], responses: { '200': { description: 'Kod se vraci vzdy. `manifest` je null, kdyz je skript rozbity.', }, '400': { description: 'Neplatne ID skriptu' }, '404': { description: 'Skript neexistuje' }, }, }, put: { tags: ['Skripty'], summary: 'Ulozit kod skriptu', description: 'Nejdriv se kod nacte a overi, az pak prepise soubor. Rozbita uprava ' + 'se neulozi a puvodni skript dal funguje.', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' }, example: 'idoklad.get-issued-invoice', }, ], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['code'], properties: { code: { type: 'string', description: 'Cely obsah souboru vcetne exportu manifest a run.', }, }, }, }, }, }, responses: { '200': { description: 'Ulozeno, vraci se overeny manifest' }, '400': { description: 'Kod nebo manifest neprosel, v `issues` je co opravit', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, '403': { description: 'Jen spravce platformy' }, }, }, }, '/api/dashboard/scripts/{id}/test': { post: { tags: ['Skripty'], summary: 'Zkusebni spusteni skriptu', description: 'POZOR: vola opravdovou sluzbu. Vystavena faktura opravdu vznikne. ' + 'Chyba skriptu neni chyba API, vraci se 200 s popisem v `error`.', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' }, example: 'idoklad.get-issued-invoice', }, ], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { inputs: { type: 'object', additionalProperties: true, example: { invoiceId: 12345 }, }, }, }, }, }, }, responses: { '200': { description: 'Vysledek behu', content: { 'application/json': { schema: { $ref: '#/components/schemas/ScriptRunResult' } }, }, }, '400': { description: 'Neplatne ID nebo vstupy' }, '403': { description: 'Jen spravce platformy' }, }, }, }, '/api/dashboard/automations': { get: { tags: ['Automatizace'], summary: 'Seznam automatizaci', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Automatizace', content: { 'application/json': { schema: { type: 'object', properties: { items: { type: 'array', items: { $ref: '#/components/schemas/Automation' } }, }, }, }, }, }, }, }, post: { tags: ['Automatizace'], summary: 'Zalozit automatizaci', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['name'], properties: { name: { type: 'string', minLength: 3 } }, }, }, }, }, responses: { '201': { description: 'Vytvoreno', content: { 'application/json': { schema: { $ref: '#/components/schemas/AutomationDetail' } }, }, }, '400': { description: 'Neplatny nazev' }, }, }, }, '/api/dashboard/automations/{id}': { parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' }, example: 'AUT-01' }, ], get: { tags: ['Automatizace'], summary: 'Detail vcetne stromu akci', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Detail', content: { 'application/json': { schema: { $ref: '#/components/schemas/AutomationDetail' } }, }, }, '404': { description: 'Neexistuje' }, }, }, put: { tags: ['Automatizace'], summary: 'Ulozit automatizaci', description: 'Validuje strom proti katalogu konektoru. Nedokoncenou automatizaci server ' + 'nezapne ani pri enabled=true, duvody vraci v poli issues.', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { name: { type: 'string', minLength: 3 }, enabled: { type: 'boolean' }, flow: { $ref: '#/components/schemas/AutomationFlow' }, }, }, }, }, }, responses: { '200': { description: 'Ulozeno', content: { 'application/json': { schema: { $ref: '#/components/schemas/AutomationDetail' } }, }, }, '400': { description: 'Neplatny strom' }, '404': { description: 'Neexistuje' }, }, }, delete: { tags: ['Automatizace'], summary: 'Smazat automatizaci', security: [{ bearerAuth: [] }], responses: { '204': { description: 'Smazano' }, '404': { description: 'Neexistuje' } }, }, }, '/api/dashboard/automations/{id}/webhook/regenerate': { post: { tags: ['Automatizace'], summary: 'Nova adresa webhooku', description: 'Stara adresa okamzite prestane fungovat.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': { description: 'Novy token', content: { 'application/json': { schema: { $ref: '#/components/schemas/AutomationDetail' } }, }, }, '404': { description: 'Neexistuje nebo spoustecem neni webhook' }, }, }, }, '/api/dashboard/notifications': { get: { tags: ['Dashboard'], summary: 'Upozorneni prihlaseneho', description: 'Cislo u zalozky Tickety a hlasky o pridelene praci. Upozorneni jsou ulozena, ' + 'takze je najde i ten, kdo mel portal zavreny.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Upozorneni', content: { 'application/json': { schema: { type: 'object', properties: { items: { type: 'array', items: { type: 'object' } }, unread: { type: 'integer' }, mine: { type: 'integer', description: 'Kolik ticketu ma volajici u sebe.' }, }, }, }, }, }, }, }, }, '/api/dashboard/notifications/read': { post: { tags: ['Dashboard'], summary: 'Oznacit upozorneni jako prectena', security: [{ bearerAuth: [] }], requestBody: { required: false, content: { 'application/json': { schema: { type: 'object', properties: { ids: { type: 'array', items: { type: 'string' }, description: 'Bez seznamu se oznaci vsechna.', }, }, }, }, }, }, responses: { '200': { description: 'Oznaceno' } }, }, }, '/api/dashboard/runs': { get: { tags: ['Automatizace'], summary: 'Stav fronty behu', description: 'Kdyz neco nefunguje, tohle je prvni misto, kam se clovek podiva: ceka fronta, ' + 'nebo uz to nekolikrat selhalo? U kazdeho behu je cele chybove hlaseni.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Fronta a posledni behy', content: { 'application/json': { schema: { type: 'object', properties: { stats: { type: 'object', properties: { pending: { type: 'integer' }, running: { type: 'integer' }, done: { type: 'integer' }, failed: { type: 'integer' }, oldestPendingAt: { type: 'string', nullable: true }, }, }, items: { type: 'array', items: { type: 'object' } }, }, }, }, }, }, }, }, }, '/webhook/ticket/{token}': { post: { tags: ['Webhook'], summary: 'Prijem udalosti do ticketu', description: 'VEREJNY endpoint, autorizuje token firmy v adrese. Se stejnym externalId se ' + 'udalost navesi na existujici ticket, jinak vznikne novy. externalId je ' + 'unikatni v ramci firmy.', parameters: [{ name: 'token', in: 'path', required: true, schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { externalId: { oneOf: [{ type: 'string' }, { type: 'number' }] }, source: { type: 'string', example: 'eshop' }, event: { type: 'string', example: 'order.created' }, subject: { type: 'string' }, typeId: { type: 'string' }, tags: { type: 'array', items: { type: 'string' } }, fields: { type: 'object', additionalProperties: true }, }, }, }, }, }, responses: { '201': { description: 'Ticket vznikl' }, '200': { description: 'Udalost se navesila na existujici ticket' }, '400': { description: 'Neplatna data' }, '404': { description: 'Neznamy token' }, }, }, }, '/webhook/{token}': { post: { tags: ['Webhook'], summary: 'Prijem dat do automatizace', description: 'VEREJNY endpoint. **Odpovi hned** (202) a strom vykona worker na pozadi - ' + 'cizi sluzba muze odpovidat pomalu a odesilateli by vyprsel timeout. ' + 'Telo se kontroluje proti kontraktu spoustece, vcetne vnorenych cest.', parameters: [{ name: 'token', in: 'path', required: true, schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', additionalProperties: true }, }, }, }, responses: { '202': { description: 'Prijato, zpracuje se na pozadi', content: { 'application/json': { schema: { type: 'object', properties: { accepted: { type: 'boolean' }, automationId: { type: 'string' }, runId: { type: 'string', nullable: true }, }, }, }, }, }, '400': { description: 'Telo neodpovida kontraktu spoustece' }, '404': { description: 'Neznamy token' }, '409': { description: 'Automatizace je pozastavena' }, }, }, get: { tags: ['Webhook'], summary: 'Napoveda: co se v tele ceka', description: 'Vraci metodu, seznam parametru vcetne cest a ukazku tela.', parameters: [{ name: 'token', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': { description: 'Kontrakt' }, '404': { description: 'Neznamy token' }, }, }, }, '/api/contact': { post: { tags: ['Kontakt'], summary: 'Odeslat poptavku z webu', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['name', 'email', 'topic', 'message'], properties: { name: { type: 'string', minLength: 2 }, email: { type: 'string', format: 'email' }, company: { type: 'string' }, phone: { type: 'string' }, topic: { type: 'string', enum: [ 'automatizace', 'voicebot', 'integrace', 'dashboard', 'podpora', 'jine', ], }, message: { type: 'string', minLength: 10 }, }, }, }, }, }, responses: { '202': { description: 'Prijato' }, '400': { description: 'Neplatny vstup' }, }, }, }, }, }; }