diff --git a/documentation/12-sluzby-a-konektory.md b/documentation/12-sluzby-a-konektory.md index ce14355..4071f27 100644 --- a/documentation/12-sluzby-a-konektory.md +++ b/documentation/12-sluzby-a-konektory.md @@ -364,3 +364,26 @@ tim mysli i uzivatel. | Historie zmen konektoru | kdo kdy prepsal udaje, se nikde neuklada | | OAuth toky | zatim jen hlavicky, obnovovani tokenu resi sluzba | | Vyber konektoru v builderu | krok uz `connectorId` nese, UI ho zatim nenabizi | + +## Co se nabizi v builderu + +Krok automatizace jde postavit jen nad sluzbou, kterou je **cim zavolat**: + +- je `general`, tedy obejde se bez konektoru (webhook, casovac, rucni + spusteni, formular, incident, ticket, HTTP, transformace, pauza, log), +- nebo k ni firma ma aspon jedno napojeni. + +Katalog se kvuli tomu **nefiltruje**. `GET /api/dashboard/services` jen prida +ke kazde sluzbe `connected` a vybira az builder. Duvod: log ticketu a detail +akce podle katalogu prekladaji ID operaci na jmena, a sluzba, ktera by +z odpovedi zmizela, by v uz zapsanem radku zustala jako hole ID. + +## Kdo vidi soukromou sluzbu + +U `visibility: restricted` **rozhoduje firma, ne clovek**. Spravce platformy +driv videl vsechny sluzby vzdycky, i po prepnuti do firmy, ktera je nema - +takze si mohl do jeji automatizace vybrat zakazkovou integraci jineho klienta. + +Kdyz je firma vybrana, plati jeji seznam: `visibility.tenantIds` nebo +zpristupneni pres `tenantHasService`. Bez vybrane firmy spravce platformy +spravuje katalog a vidi vsechno. diff --git a/documentation/99-zmeny.md b/documentation/99-zmeny.md index 5642099..6a66876 100644 --- a/documentation/99-zmeny.md +++ b/documentation/99-zmeny.md @@ -2,6 +2,47 @@ Nejnovejsi nahore. +## 2026-09-02 - Builder nabizi jen to, co jde zavolat + +V nabidce kroku byly vsechny sluzby katalogu, i ty, ke kterym firma nema +napojeni. Slo tedy vybrat Raynet CRM bez konektoru a postavit strom, ktery pri +prvnim behu spadne na chybejicich udajich - a to se pozna az za tyden, kdyz +prijde prvni ostra udalost. + +### Pravidlo + +Nabizi se sluzba, ktera je **obecna, nebo k ni firma ma napojeni**. + +Obecne jsou ty, co se bez konektoru obejdou: webhook, casovac, rucni spusteni, +formular, incident, ticket, HTTP, transformace, pauza a zapis do logu. Ma to uz +priznak `general`, jen ho nikdo nepouzil na filtrovani nabidky. + +### Katalog se nefiltruje, jen se oznacuje + +`GET /api/dashboard/services` prida ke kazde sluzbe `connected`, tedy jestli +k ni firma ma aspon jedno napojeni. Sluzba z odpovedi **nemizi**: log ticketu +a detail akce podle katalogu prekladaji ID operaci na jmena, a kdyby zmizela, +zustalo by v uz zapsanem radku hole ID. Filtruje az builder. + +Aby nabidka nevypadala jako cely katalog, je pod ni veta, kolik sluzeb ceka na +napojeni. Bez ni to vypada, ze sluzba neexistuje, misto ze k ni chybi udaje. + +### Soukroma sluzba uz neni videt cizi firme + +`canSeeService` vracelo spravci platformy `true` driv, nez se vubec podivalo na +viditelnost sluzby. Zakazkova integrace omezena na jednoho klienta se tak +ukazovala i po prepnuti do jine firmy - spravce si ji mohl vybrat do jeji +automatizace. + +**Rozhoduje firma, ne clovek.** Kdyz je firma vybrana, plati jeji seznam; bez +vybrane firmy spravce platformy spravuje katalog a vidi vsechno. Zaroven se +konecne pouziva `tenantHasService`, tedy zpristupneni sluzby firme pres +nastaveni - dosud to bylo pole, ktere nikdo necetl. + +Overeno: Polstryn SAP je pro `tnt_logitrans` v katalogu, pro `tnt_automia` uz +ne, a to i pro spravce platformy. V nabidce builderu pro `tnt_automia` zbylo +deset obecnych sluzeb plus iDoklad, na ktery firma napojeni ma. + ## 2026-09-02 - Tickety maji zalozky, fronta umi prirazovat Seznam ticketu mel dva prepinace, "Moje tickety" a "Ve fronte", schovane mezi diff --git a/src/data/services.ts b/src/data/services.ts index a0b42a0..14a59c6 100644 --- a/src/data/services.ts +++ b/src/data/services.ts @@ -21,6 +21,7 @@ import type { FieldType } from './conditions.js'; import type { User } from '../types.js'; import { isMcpService, MCP_EASYWEB_SERVICE_ID, MCP_SERVICE_ID } from '../mcp/dialect.js'; +import { tenantHasService } from './tenantFeatures.js'; export type ServiceCategory = /** Obecne veci, ktere ma kazdy. Nepotrebuji konektor. */ @@ -2796,16 +2797,26 @@ export function findService(serviceId: string): Service | undefined { */ export function canSeeService(service: Service, user: User, tenantId: string | null): boolean { if (service.general) return true; - if (user.platformAdmin) return true; switch (service.visibility.mode) { case 'everyone': return true; case 'admin': - return false; - case 'restricted': + return user.platformAdmin; + case 'restricted': { + // Zakazkova integrace pro konkretniho cloveka, at uz je prepnuty kamkoliv. if (service.visibility.userIds.includes(user.id)) return true; - return tenantId !== null && service.visibility.tenantIds.includes(tenantId); + + /* + * **Rozhoduje firma, ne clovek.** Spravce platformy driv videl vsechny + * sluzby vzdycky, i po prepnuti do firmy, ktera je nema - takze si mohl + * do jeji automatizace vybrat zakazkovou integraci jineho klienta. + * Kdyz je firma vybrana, plati jeji seznam; bez vybrane firmy spravuje + * spravce platformy katalog a vidi vsechno. + */ + if (tenantId === null) return user.platformAdmin; + return service.visibility.tenantIds.includes(tenantId) || tenantHasService(tenantId, service.id); + } default: return false; } diff --git a/src/routes/dashboard.ts b/src/routes/dashboard.ts index 7baf168..d65cd3a 100644 --- a/src/routes/dashboard.ts +++ b/src/routes/dashboard.ts @@ -20,6 +20,7 @@ import { import { operatorAllowedForType, operatorsByType } from '../data/conditions.js'; import { listTenantScripts } from '../data/tenantScripts.js'; import { + connectorCountsByService, defaultConnectorFor, getConnector, storageStatus, @@ -1032,7 +1033,7 @@ dashboardRouter.use('/helpdesk', helpdeskRouter); * Vraci se **jen sluzby, ktere uzivatel vidi**. Neviditelna sluzba v odpovedi * neni vubec, ne se stavem "nemate pravo". */ -dashboardRouter.get('/services', (req, res) => { +dashboardRouter.get('/services', async (req, res) => { const access = accessFor(req.user!); const requested = typeof req.query.tenantId === 'string' ? req.query.tenantId : undefined; const tenantId = requested ?? access.defaultTenantId; @@ -1040,12 +1041,24 @@ dashboardRouter.get('/services', (req, res) => { const visible = new Set(visibleServices(req.user!, tenantId).map((service) => service.id)); const tenantIds = tenantId ? [tenantId] : []; + /* + * Ke ktere sluzbe ma firma napojeni. + * + * Katalog se **nefiltruje**, jen se oznaci: log ticketu a detail akce + * podle nej prekladaji ID operaci na jmena, a kdyby sluzba z odpovedi + * zmizela, zustalo by v uz zapsanem radku holé ID. Vybirat z ni ale nema + * smysl, kdyz ji neni cim zavolat - o to se stara builder. + */ + const counts = await connectorCountsByService(tenantIds); + res.json({ categories: serviceCategories, items: withRuntimeOptions( // Firma se predava kvuli MCP: nastroje jsou vlastnost jejiho napojeni, // ne katalogu. Bez ni se nevrati zadne. - serviceCatalog(tenantId ?? null).filter((service) => visible.has(service.id)), + serviceCatalog(tenantId ?? null) + .filter((service) => visible.has(service.id)) + .map((service) => ({ ...service, connected: (counts.get(service.id) ?? 0) > 0 })), { people: listPeople(tenantIds).map((person) => ({ id: person.id, name: person.name })), groups: listGroups(tenantIds).map((group) => ({ id: group.id, name: group.name })), diff --git a/web/src/components/dashboard/flow/StepPicker.tsx b/web/src/components/dashboard/flow/StepPicker.tsx index 89b8edd..f9ce7ab 100644 --- a/web/src/components/dashboard/flow/StepPicker.tsx +++ b/web/src/components/dashboard/flow/StepPicker.tsx @@ -46,9 +46,30 @@ export function StepPicker({ const operationsOf = (connector: Service) => mode === 'trigger' ? connector.triggers : connector.actions; - // Konektory, ktere v tomhle rezimu vubec maji co nabidnout. + /* + * Co jde ve strome opravdu pouzit. + * + * Krome toho, ze sluzba v tomhle rezimu neco nabizi, musi jit **zavolat**: + * bud je obecna (webhook, pauza, ticket), nebo k ni firma ma napojeni. + * Vybrat Raynet bez konektoru znamenalo postavit strom, ktery pri prvnim + * behu spadne na chybejicich udajich - a to se pozna az za tyden. + */ const usable = useMemo( - () => connectors.filter((connector) => operationsOf(connector).length > 0), + () => + connectors.filter( + (connector) => + operationsOf(connector).length > 0 && (connector.general || connector.connected), + ), + [connectors, mode], + ); + + /** Kolik sluzeb je schovanych jen proto, ze k nim chybi napojeni. */ + const missing = useMemo( + () => + connectors.filter( + (connector) => + operationsOf(connector).length > 0 && !connector.general && !connector.connected, + ).length, [connectors, mode], ); @@ -264,6 +285,17 @@ export function StepPicker({ > )} + + {/* + Aby bylo poznat, ze nabidka neni cely katalog. Bez teto vety by to + vypadalo, ze sluzba neexistuje, misto ze k ni chybi napojeni. + */} + {!selected && missing > 0 && ( +
+ Dalších {missing} služeb se objeví, až k nim přidáte napojení v Konektorech. + Bez přístupových údajů by krok při prvním běhu spadl. +
+ )} ); diff --git a/web/src/types/dashboard.ts b/web/src/types/dashboard.ts index 23e5c9b..50753c6 100644 --- a/web/src/types/dashboard.ts +++ b/web/src/types/dashboard.ts @@ -324,6 +324,14 @@ export interface Service { status: ServiceStatus; /** true = funguje bez konektoru (webhook, pauza, transformace dat). */ general: boolean; + /** + * true = firma ma k teto sluzbe aspon jedno napojeni. + * + * Katalog se kvuli tomu nefiltruje - log ticketu podle nej preklada ID + * operaci na jmena a sluzba, ktera by z odpovedi zmizela, by v uz zapsanem + * radku zustala jako hole ID. Vybirat z ni ale nema smysl. + */ + connected: boolean; appId: string | null; /** * `smtp` = sluzba se nevola pres HTTP, adresu nese konektor v udajich.