Navrh pristupneho portalu a srovnani vzorove automatizace s instanci

Dve veci, obe bez zmeny chovani aplikace.

Navrh (documentation/25-navrh-pristupny-portal.md):

Vzniklo z otazky, jak portal priblizit cloveku, ktery ho nikdy nevidel.
Odpoved se rozpadla na pet veci, ktere spolu souvisi vic, nez to vypada:

- prehled ukazuje jen cisla, zadna slovesa. Vsech sest widgetu ve vychozi sade
  jsou statistiky a seznamy. Navrh pridava "Moje tickety", "Fronta bez
  resitele", "Co potrebujete udelat" a "Zaciname". Prvni dva jsou skoro zadarmo,
  builtinSources uz ten mechanismus maji
- formulare nemaji spolecnou vrstvu. inputClass je nadefinovany na 13 mistech
  a rozesel se do peti ruznych vzhledu, Field je napsany trikrat. V ui/ neni
  zadny formularovy prvek. Blokuje to widget akci, protoze NewTicketDialog je
  ten modal a formular z nej vytahnout nejde
- hledani neumi to jedine, k cemu je. Klientsky filtr nehleda v obsahu, ve
  vlastnich polich ani v externim ID, takze hovor podle callSid se dohledat
  neda. Navrh je modal s kriterii, protoze ticket nema pevnou sadu poli
- viditelnost ticketu se neda omezit. Pohled tenant dostane kazdy, kdo do firmy
  patri, mine je dobrovolny filtr a ne strop. Navrh vede viditelnost pres
  clenstvi (priznak na firme, priznak u kazde skupiny), ne pres role - role jsou
  na celou firmu a neumi rict "v jedne sekci vidim vse, v druhe svoje"
- uloziste neprezije nasazeni, coz podpira bod o hledani

Dve veci, ktere stoji za zapamatovani, i kdyby se navrh nikdy nedodelal:
strop viditelnosti nepatri do hledani, ale do cteni ticketu (cesty k ticketum
jsou tri a dve z nich filtruji tickets primo), a pohled a strop nejsou totez.

Ctyri otevrene otazky jsou v zaveru navrhu.

Vzorova automatizace (src/data/automationStore.ts):

seedRealAutomations drzelo starsi podobu stromu nez ta, ktera na instanci
opravdu bezi. Protoze data neprezivaji redeploy, je tenhle seed jedine misto,
kde nastaveni prezije nasazeni - kdyz se rozejde, znamena to po kazdem nasazeni
stavet strom rucne znovu.

Opsano z bezici instance: spoustec ma sest parametru misto tri (pribylo result,
rating a data), krok upsert pise do obsahu {{voicebotId}}, {{data}} a stav bere
z {{result}}, a za nim je podminka nad vysledkem - cokoliv krome "Chybějící
informace" ticket zavre, jinak jde na servicedesk s vysokou prioritou.

Overeno lokalnim startem s vlastnim DATA_DIR: automatizace se nasype, ma ctyri
kroky stejne jako instance, je zapnuta a nema zadny nedodelek.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-09-02 09:37:46 +02:00
co-authored by Claude Opus 5
parent 1134852bff
commit 72da07debe
5 changed files with 644 additions and 2 deletions
+2
View File
@@ -20,6 +20,8 @@ popisuje, **jak je postavena a proc tak**.
| krok automatizace | [05-dashboard-a-builder.md](05-dashboard-a-builder.md), [20-fronta-a-runtime.md](20-fronta-a-runtime.md) |
| tickety a helpdesk | [06-tickety.md](06-tickety.md), [18-ticketovaci-system.md](18-ticketovaci-system.md) |
| prava a firmy | [07-firmy-a-prava.md](07-firmy-a-prava.md), [17-nastaveni-a-prava.md](17-nastaveni-a-prava.md) |
| prehled a widgety | [08-dashboard-widgety.md](08-dashboard-widgety.md), [25-navrh-pristupny-portal.md](25-navrh-pristupny-portal.md) |
| formulare v portalu | [25-navrh-pristupny-portal.md](25-navrh-pristupny-portal.md), sekce 2 |
| uloziste | [14-databaze.md](14-databaze.md) |
| texty a jazyky | [23-jazyky.md](23-jazyky.md) |
| vzhled | [22-znacka-a-design.md](22-znacka-a-design.md) |
+1
View File
@@ -148,4 +148,5 @@ pro frontu, beh kroku a rozpocet na 150 klientu, a
| [22-znacka-a-design.md](22-znacka-a-design.md) | znacka WorkNuke, tokeny, prvky |
| [23-jazyky.md](23-jazyky.md) | prepinani jazyku a slovniky |
| [24-mcp-konektory.md](24-mcp-konektory.md) | MCP servery firmy jako kroky |
| [25-navrh-pristupny-portal.md](25-navrh-pristupny-portal.md) | **navrh**: prehled, formulare, hledani, viditelnost |
| [99-zmeny.md](99-zmeny.md) | zaznam zmen, nejnovejsi nahore |
+545
View File
@@ -0,0 +1,545 @@
# 25 - Navrh: pristupny portal a viditelnost
**Navrh, ne popis stavu. Nic z toho zatim neni naprogramovane.** Az se cast
udela, prepise se do prislusneho souboru dokumentace a odsud zmizi. Stejne
pravidlo jako u [09-navrh-rozsireni.md](09-navrh-rozsireni.md).
Popis soucasneho stavu je v [01-prehled-a-stav.md](01-prehled-a-stav.md), prava
a pohledy v [07-firmy-a-prava.md](07-firmy-a-prava.md), widgety
v [08-dashboard-widgety.md](08-dashboard-widgety.md).
## Z ceho to vzniklo
Otazka znela: jak portal priblizit cloveku, ktery ho nikdy nevidel. Z te otazky
vypadlo pet veci, ktere spolu souvisi vic, nez to na prvni pohled vypada:
1. prehled ukazuje jen cisla, zadna slovesa,
2. formulare nemaji spolecnou vrstvu, takze kazda stranka pise vlastni,
3. hledani neumi to jedine, k cemu je potreba,
4. viditelnost ticketu se neda omezit, protoze pohled si voli klient,
5. uloziste v kontejneru neprezije nasazeni.
Poradi neni nahodne. Widget akci potrebuje formulare, hledani potrebuje
viditelnost, a hledani zpetne potrebuje uloziste, ktere prezije nasazeni.
---
## 1 - Prehled potrebuje slovesa
### Cim to je
Vychozi rozlozeni ma sest widgetu a vsech sest jsou **cisla a seznamy**: aktivni
automatizace, otevrene tickety, incidenty, graf behu, posledni tickety,
incidenty. Ani jeden neni sloveso. Kdo prijde poprve, dozvi se stav, ale ne co
s tim.
Akce existuji, jen jsou o kliknuti dal a schovane za podstatnym jmenem:
- "Novy ticket" je tlacitko na `/dashboard/tickety`,
- "Nahlasit problem" je na `/dashboard/helpdesk`,
- hledani je pole uvnitr seznamu ticketu.
Navigace je pojmenovana podstatnymi jmeny (Tickety, Automatizace, Konektory). To
je spravne pro toho, kdo system zna, a k nicemu pro toho, kdo prisel poprve
a premysli ve slovesech: chci neco nahlasit, chci neco najit.
### Co se s tim nesmi udelat
**Ne sada widgetu, jeden na akci.** Nabidka by se zaplnila sesti skoro stejnymi
radky a novy clovek si widget stejne neprida - je to posledni clovek, ktery
otevre "Upravit dashboard". O tom, co uvidi, nerozhoduje katalog, ale **vychozi
rozlozeni**.
### Widget "Moje tickety" a "Fronta bez resitele"
Nejdulezitejsi dve dlazdice a zaroven nejlevnejsi. Vsechny dnesni widgety
odpovidaji na "jak jsme na tom". Zadny na "co mam delat ted".
Mechanismus uz existuje a pouziva ho "Vykon resitelu": `builtinSources`
v `src/data/widgets.ts` da vestavenemu widgetu zdroj dat a spocita ho tatáz
cesta jako u vlastnich widgetu, tedy `/api/dashboard/widget-data`.
```ts
'list.myTickets': { kind: 'ticketList', filter: { assignee: ['me'] }, limit: 8 },
'list.unassigned': { kind: 'ticketList', filter: { assignee: ['unassigned'] }, limit: 8 },
```
`WidgetSource` uz `ticketList` umi a filtr `assignee` zna hodnoty `me`
i `unassigned`. Klient uz umi vykreslit hodnotu `kind: 'tickets'`. Takze dva
zaznamy v katalogu, dva ve zdrojich, dva radky v `COMPUTED_BUILTINS`
v `Overview.tsx`. **Zadna nova komponenta.** Typ `builtinSources` se rozsiri
z dnesniho `{ kind: 'agentStats' }` na `WidgetSource`.
Ctyri veci k rozhodnuti:
- **Kdo nema navazaneho resitele, tomu se dlazdice nesmi nabidnout.**
`access.personId` muze byt null a takovy clovek uvidi prazdno navzdy a nedozvi
se proc. `widgetCatalog(tenantIds, userId)` uz `userId` dostava, takze se da
z nabidky vyfiltrovat.
- **Prazdny stav je dobra zprava**, ne chyba. "Nemate nic u sebe" plus odkaz do
fronty. Prazdna karta vypada jako rozbita.
- **Razeni.** `listTickets` uz radi nevyrizene nahoru a uvnitr podle posledni
zmeny, takze vyrizene samy klesnou dolu a pri limitu 8 se skoro neukazou. To
staci. Kdyby melo razeni byt "co hori" (priorita, pak nejdele beze zmeny), je
to rozsireni zdroje - `WidgetTicketFilter` prioritu vubec nezna.
- **Pocet v titulku by chtel filtr na vyrizene.** `ticketCount` dnes zapocita
i uzavrene, protoze `matches()` ve `widgetData.ts` o priznaku `closed` nevi.
Stav filtrovat jde, jenze stav je volny retezec a vyjmenovavat ho je presne to,
cemu se projekt vyhnul. Reseni je `closed?: boolean` ve `WidgetTicketFilter`
a jeden radek v `matches`. Prospeje to i vlastnim widgetum: dnes se neda
postavit "otevrene tickety podle typu".
### Widget "Co potrebujete udelat"
Jeden widget, novy `kind: 'actions'`, cela sirka, **prvni ve vychozim rozlozeni**.
Uvnitr velke dlazdice, jejichz seznam se pocita z prav, ne natvrdo.
`access.permissions` a `access.nav` klient uz dostava.
| Dlazdice | Podminka | Co udela |
| ------------------- | ------------------------- | ---------------------------------------- |
| Novy ticket | `ticket.create` | otevre formular rovnou na prehledu |
| Nahlasit problem | `helpdesk.create` a modul | otevre helpdeskovy formular na miste |
| Moje tickety | `personId` neni null | `/dashboard/tickety?assignee=me` |
| Fronta bez resitele | `ticket.assign.self` | `/dashboard/tickety?assignee=unassigned` |
| Nova automatizace | `automation.edit` | builder s prazdnym stromem |
Dve pravidla:
**Dlazdice maji delat, ne odkazovat.** Kde uz formular existuje, otevrit ho primo
na prehledu. Poslat cloveka na jinou stranku a doufat, ze tam to tlacitko najde,
je presne ten problem, ktery se resi.
**Zobrazit nejvys ctyri.** Sest dlazdic je zase jen dalsi seznam. Kdyz clovek
nema pravo na nic, widget se nevykresli vubec.
Past: "Novy ticket" a "Nahlasit problem" jsou pro noveho cloveka totez, i kdyz
jsou to dve ruzne role (resitel a zadavatel). Vedle sebe ho zmatou. Bud je
rozlisit textem pod dlazdici ("resime my" versus "posilame dodavateli"), nebo
ukazat jen tu, ktera pro danou firmu dava smysl.
### Widget "Zaciname"
Cislovany postup, kde kazdy krok vede tam, kde se dela, a widget **sam zmizi**,
jakmile je hotovo:
```text
1. Napojte sluzbu -> /dashboard/konektory
2. Postavte automatizaci -> /dashboard/automatizace
3. Poslete testovaci data -> adresa webhooku ke zkopirovani
4. Prisel prvni ticket -> /dashboard/tickety
```
Firma bez dat dnes ukazuje same nuly a plochy graf. To je horsi prvni dojem nez
prazdna plocha, protoze to vypada jako rozbite, ne jako nove. Tohle ten stav
vyuzije misto aby ho maskovalo.
Potrebuje jeden endpoint, ktery odpovi na ctyri otazky: ma firma konektor, ma
automatizaci, probehl beh, existuje ticket. Data pro to vsechna existuji.
### Vychozi rozlozeni
Dnes sest polozek. S akcemi, mymi tickety a frontou je jich devet, coz uz je
dlouha stranka pro nekoho, kdo prisel poprve. Navrh:
```text
1. Co potrebujete udelat cela sirka
2. Moje tickety polovina
3. Fronta bez resitele polovina
4. Otevrene tickety tretina
5. Bezici incidenty tretina
6. Aktivni automatizace tretina
7. Posledni tickety polovina
8. Incidenty polovina
```
Graf behu z vychozi sady ven. Je to nejmene srozumitelna dlazdice pro noveho
cloveka (behy ceho a co s tim) a zabira celou sirku. V katalogu zustane.
**Zmena vychozi sady se projevi jen tem, kdo si dashboard jeste neupravili.**
Rozlozeni se uklada za dvojici uzivatel a firma. Kdo uz si ho osahal, nove
dlazdice neuvidi, dokud si je neprida nebo nedá "Vychozi". Pro nove uzivatele,
coz je cil, to sedi.
---
## 2 - Formularova vrstva
### Cim to je
V `components/ui/` je Badge, Button, Card, Container, Modal, PageHeader, Section
a Spinner. **Zadny formularovy prvek.** Takze si ho kazda stranka pise znovu.
`inputClass` je nadefinovany na **13 mistech** a rozesel se do peti vzhledu:
| Kde | Pozadi | Zaobleni | Vypln | Placeholder |
| ------------------------------------ | --------- | -------- | --------------- | ----------- |
| EntityAdmin, InvitePanel, Connectors | `ink-850` | `lg` | `px-3 py-2` | `white/25` |
| TenantScripts, Helpdesk | `ink-900` | `lg` | `px-3 py-2` | `white/25` |
| NewTicketDialog | `ink-900` | `lg` | `px-3 py-1.5` | `white/25` |
| Login, Contact | `ink-850` | `xl` | `px-4 py-3` | `white/30` |
| Invite | `ink-850` | `xl` | `px-3.5 py-2.5` | `white/30` |
Stejne vstupni pole vypada jinak podle toho, kde v aplikaci stojite. K tomu:
- komponenta `Field` je napsana trikrat (NewTicketDialog, Contact, Connectors)
a `FieldInput` dvakrat, pokazde neexportovana,
- seznam priorit je zkopirovany v `NewTicketDialog` i `Helpdesk`,
- kazdy z osmi souboru s formularem si zvlast pise tutez dvanactku radku:
`saving`, try/catch nad `apiFetch`, `setError`, `reset`, `onSuccess`.
### Proc to blokuje widget akci
Dlazdice ma otevrit "Novy ticket" a "Nahlasit problem" rovnou z prehledu.
`NewTicketDialog` ale **je** ten modal, formular z nej vytahnout nejde.
A helpdeskovy formular neexistuje jako komponenta vubec, je to kus JSX uvnitr
stranky. Takze bud kopie potreti, nebo predelavat.
Proto formularova vrstva **pred** widgety, ne po nich.
### Navrh
**`components/ui/form/`** - `Field` (popisek, napoveda, chyba), `Input`,
`Textarea`, `Select`, `FormError`. Tridy na jednom miste.
**Dve velikosti, ne jedna.** Ten drift v tabulce neni jen neporadek, jsou v nem
dva skutecne kontexty: huste formulare v portalu (`py-1.5` az `py-2`) a vzdusne
na verejnem webu (`py-3`). Sjednotit je do jedne velikosti by Login zhorsilo.
Takze `size="sm" | "md"` a `ink-850` versus `ink-900` podle toho, jestli pole
stoji na karte nebo na pozadi.
**Formular zvlast od sveho obalu.** Na tomhle stoji znovupouzitelnost:
```text
NewTicketForm pole, validace, odeslani
NewTicketDialog Modal plus NewTicketForm
```
Widget akci pouzije `NewTicketForm` primo, stranka ticketu dal `NewTicketDialog`.
Totez pro `HelpdeskRequestForm`, ktery se z `Helpdesk.tsx` vytahne ven.
Pravidlo: **stranka drzi nacitani dat a rozvrzeni, formular drzi pole, validaci
a odeslani.** Stranka nesmi vedet, jak vypada vstup pro predmet.
**Ciselniky ven.** Priority jsou pevny vycet, patri do jednoho `lib/options.ts`.
Stavy a kanaly uz server nabizi pres `/widget-data/options`, ty se maji brat
odtamtud.
**`useSubmit`** na tu opakovanou dvanactku radku. Ne kvuli abstrakci, ale proto,
ze dnes se v kazdem formulari muze chyba osetrit jinak, a taky se to deje.
### Co nedelat
**Zadnou formularovou knihovnu.** Formulare jsou tady male, react-hook-form se
zod resolverem by prinesl dve zavislosti a vlastni zpusob mysleni kvuli osmi
polim. Sedi to i na to, jak je projekt psany jinde: u druhu widgetu stoji
v komentari, ze jsou schvalne obecne a je jich malo.
**Neprepisovat vsech 13 mist najednou.** `EntityAdmin`, `Connectors` a `Scripts`
jsou velke a s widgety nesouvisi. Prevest to, ceho se dotykame (ticket, helpdesk)
plus verejne stranky, kde je drift videt nejvic, a zbytek nechat doputovat, jak
se k nemu bude sahat.
---
## 3 - Hledani jako modal s kriterii
### Kde hledani patri
**Do sekce, ne do horni listy portalu.** Tohle neni eshop, kde je hledani hlavni
zpusob navigace. Ticket se najde pres frontu, pres "moje", pres filtr. Hleda se
az ve chvili, kdy nekdo potrebuje dohledat, co se stalo v kvetnu. Tomu odpovida
tlacitko v hlavicce sekce Tickety, vedle "Novy ticket" a se stejnou vahou.
Hledani ma byt **schopne, ale tiche**.
### Proc modal s kriterii a ne jedno pole
**Ticket nema pevnou sadu poli.** Krome predmetu, obsahu, stavu, stitku, kanalu,
zakaznika, resitele a skupiny nese vlastni pole sveho typu, a tech muze byt kolik
si firma nadefinuje. K tomu udalosti s celym prijatym telem. Jedno textove pole
tohle neobslouzi, protoze uzivatel nema jak rict, jestli `3` je cislo objednavky,
castka nebo kus predmetu.
Kriteria proto musi byt **dynamicka podle vybraneho typu**: vyberu typ Objednavka
a teprve pak se objevi pole "cislo objednavky" a "castka".
### Co dnesni hledani umi
Klientsky filtr pres ctyri veci: `id`, `subject`, `customer.company`,
`customer.contact` (`Tickets.tsx`). Nehleda tedy v **obsahu**, ve **vlastnich
polich**, ve **stitcich** ani v **externim ID**. To posledni je zrovna to, cim se
dohledava hovor: clovek ma `CAbc75a8...` a chce ten ticket. Dnes ho nenajde.
A funguje to jen proto, ze `GET /api/dashboard/tickets` vraci **vsechny tickety
firmy najednou**, bez limitu a bez strankovani. Pri par desitkach to nevadi, pri
deseti tisicich jsou to megabajty do prohlizece pri kazdem otevreni seznamu.
Hledani pres modal proto znamena **serverovy endpoint**, a je to zaroven
prilezitost prestat posilat vsechno.
### Tvar
**Modal je jen zadani.** Kriteria:
- text a k nemu volba kde (predmet, obsah, externi ID, kontakt, vse),
- obdobi od do,
- stav, priorita, kanal, stitky,
- resitel nebo skupina, vcetne "bez resitele",
- vyrizene: jen otevrene, jen vyrizene, oboji,
- typ ticketu, a po jeho vyberu **jeho vlastni pole**.
**Vysledek patri do stranky, ne do modalu.** V modalu se s nalezem neda pracovat,
neda se z nej proklikat na detail a zpatky. Modal se po odeslani zavre a seznam
pod nim ukaze nalez plus listu "hledano podle: ..." s krizkem.
**Kriteria do URL.** Dnes `query` v adrese vubec neni, takze nalez nejde poslat
kolegovi ani se k nemu vratit pres zpet. U nastroje na dohledavani je to polovina
uzitku.
Ulozena hledani zatim ne. Az se ukaze, ktere tri dotazy lidi poustej porad dokola.
Tenhle modal je nejlepsi argument pro formularovou vrstvu: kriteria se meni podle
typu, takze se to bez `Field`, `Input` a `Select` napise jako tisic radku JSX
s okopirovanymi tridami.
---
## 4 - Viditelnost: kdo ktere tickety vidi
### Cim to je
```ts
if (tenants.length > 0) scopes.push('tenant');
```
`access.ts`. Pohled na celou firmu dostane **kazdy, kdo do ni patri**, bez ohledu
na roli. A `ticket.view` se nepouziva k tomu, ktere tickety uvidite, ale jen
k tomu, jestli se vam zobrazi zalozka.
Takze dnes: resitel s roli `agent` posle `?scope=tenant` a dostane vsechny tickety
firmy. `mine` je **dobrovolny filtr, ne strop**. Klient si voli pohled a server
mu veri.
Rozdil, na kterem to cele stoji: **pohled je co chci videt, strop je co vubec smim
videt.** Dnes existuje jen pohled.
### Proc to neresit rolemi
Role jsou na clenstvi ve **firme**:
```ts
interface Membership { tenantId: string; roleIds: string[] }
```
Takze role nikdy nedokaze rict "v Servicedesku vidim vse, v Uctarne jen svoje" -
je jedna na celou firmu.
Delba, ktera z toho plyne a drzi se toho, co v projektu uz je:
- **Role rika, co smim delat.** Skoro vsechna prava v katalogu jsou slovesa:
`create`, `comment`, `assign`, `status.change`.
- **Clenstvi rika, co vidim.** Firemni priznak plus priznaky u skupin.
- `ticket.view` zustane tim, cim je dnes: mam vubec pristup k modulu tickety.
Nemichat to je dulezite. Kdyby viditelnost sla i pres role i pres skupiny, driv
nebo pozdeji si budou odporovat a nikdo nepozna, co plati.
### Model
Firma je uz v modelu ta velka skupina, ktera prekryva vsechny ostatni. Jmenuje se
tenant a `tenantIds` je v kazdem filtru. Nezavadi se nova uroven, jen se
pojmenovava ta, co tam je.
**Priznak na clenstvi ve firme.** Admin oznaci cloveka jako "vidi vse na firme".
Patri na `Membership`, ne na `Person`: je to postaveni uctu ve firme, ne vlastnost
resitele, a clovek muze byt ve dvou firmach jednou reditel a jednou brigadnik.
**Priznak na clenstvi ve skupine.** Skupina je dnes
`{ tenantId, name, personIds: string[] }`, clenstvi je hole ID, takze na nej nejde
nic povesit. Dve cesty:
```ts
// a) levnejsi, ale dve pole, ktera se muzou rozejit
personIds: string[]
seesAllIds: string[] // podmnozina, kterou nic nehlida
// b) jedno misto pravdy
members: Array<{ personId: string; seesAll: boolean }>
```
Doporuceni je **b**. `personIds` se cte na sesti mistech (`people.ts`,
`dashboard.ts`, `settings.ts`, `builtinSteps.ts`, `People.tsx`, seed), takze je to
hodina prace a ne migrace, ktere by se clovek bal. U varianty a) vznikne za mesic
skupina, kde nekdo "vidi vse" a pritom v ni neni.
Diky tomu, ze priznak visi na clenstvi, plati **clovek muze byt v N skupinach
a v kazde mit jina prava**. To role neumi a je to hlavni duvod, proc jit touhle
cestou.
### Vypocet stropu
Sjednoceni, ne prunik:
```text
vidi vse na firme -> cela firma
jinak -> moje tickety
+ vse ze skupin, kde mam zaskrtnuto
+ fronta bez resitele tech skupin
```
Posledni radka je rozhodnuti, ne odvozeni: bez ni nema vedouci co rozdelovat,
protoze neprirazeny ticket nepatri nikomu.
### Past: dve identity
Projekt ma **uzivatele** (kdo se prihlasi) a **resitele** (na koho jde ticket),
spojene e-mailem. Skupiny obsahuji resitele, clenstvi ve firme ma uzivatel.
Strop se proto musi pocitat v prostoru resitelu, protoze tickety odkazuji na ne,
a uzivatel se na resitele prevede jednou, na kraji - `resolveScope` to uz dela,
`access.personId`.
Dusledek, ktery je potreba vyslovit: **kdo nema navazaneho resitele, nevidi nic**,
protoze nema ani svoje tickety, ani clenstvi ve skupine. S firemnim priznakem to
pujde obejit vedome, coz je spravne: reditel resitel byt nemusi.
### Kde se to musi vynutit
Nejdulezitejsi bod celeho navrhu: **strop nepatri do hledaciho endpointu, patri
do cteni ticketu.** Kdyby ho aplikovalo jen hledani, obejde se seznamem, widgetem
nebo souhrnem.
Cesty k ticketum jsou dnes tri a jsou nezavisle:
| Cesta | Kdo ji pouziva |
| --------------------- | ------------------------------------------------------ |
| `listTickets(filter)` | seznam, helpdesk, widgety `ticketList` a `ticketCount` |
| `getWorkload(...)` | vytizeni tymu, filtruje `tickets` primo |
| `getAgentStats(...)` | vykon resitelu, filtruje `tickets` primo |
Strop ma byt soucasti toho, co vraci `resolveScope`, napriklad
`visibility: { kind: 'all' | 'groups' | 'own', personIds, groupIds }`, a
`listTickets` ho ma brat jako **povinnou** cast filtru, aby se na nej neslo
zapomenout. `getWorkload` a `getAgentStats` je dobra prilezitost prevest na
`listTickets`, aby ta cesta byla jedina.
Z toho plyne i to, ze `panel.workload` a `panel.agents` ukazuji cizi lidi a jejich
cisla. Pracovnikovi se nemaji nabidnout vubec, tedy tentyz strop filtruje
i katalog widgetu.
### Dopad na hledani
- Roletky resitel a skupina se plni **jen v rozsahu stropu**. Pracovnik tam nema
mit seznam kolegu, uz jen ten seznam je informace.
- Kdyz je strop `own`, modal to rekne nahore ("hledate ve svych ticketech"), ne
aby tise vratil min vysledku. Je to stejne pravidlo, jake `resolveScope` uz
dodrzuje u firem: radeji chyba nebo jasna veta nez tiche zuzeni.
- Vlastni pole typu se do kriterii nabizeji podle typu, ktere firma ma, ne podle
toho, co clovek vidi. Tam strop nehraje roli.
### Rozhrani
Skupiny se dnes edituji pres obecny `EntityAdmin` s polem typu multiselect, tedy
jeden seznam jmen. "Rozkliknu skupinu, vidim lidi, u kazdeho zaskrtnu" tenhle
obecny editor neumi a ani by nemel, je schvalne obecny.
Znamena to vlastni panel skupiny: hlavicka, seznam clenu, u kazdeho prepinac,
plus pridani a odebrani clena. Mala obrazovka, ale je to obrazovka, ne pole navic.
### Videt vse a smet to nastavovat jsou dve veci
Viditelnost je priznak, sprava lidi a skupin je pravo, ktere v katalogu uz je.
Nechat z toho dve. Jinak plati, ze prvni clovek, kteremu se zapne firemni
viditelnost, si tim zaroven muze rozdat cokoliv dalsimu, a to je vec, kterou chce
admin povolit vedome, ne jako vedlejsi ucinek.
---
## 5 - Uloziste
Zive nasazeni dnes hlasi:
```json
{ "mode": "file", "lostOnRedeploy": true }
```
s duvodem, ze `DATABASE_URL` neni nastavena a data se ukladaji do souboru
v `/app/data`, tedy uvnitr kontejneru. Overeno v praxi: po nasazeni 2026-09-02
zustaly ve firme dva tickety, predtim jich byly tisice.
Je to tady proto, ze to podpira bod 3. Nastroj na zpetne dohledavani nema smysl
nad ulozistem, ktere kazde nasazeni vymaze. Prechod na Postgres je planovany krok,
viz [14-databaze.md](14-databaze.md); do te doby se hledani da stavet, jen se na
nem neda nic overit do hloubky.
---
## Poradi praci
1. **Formularove primitivy** a rozdeleni dvou formularu (ticket, helpdesk). Nic
dalsiho na nich nestoji, ale stoji na nich vsechno ostatni v rozhrani.
2. **"Moje tickety" a "Fronta bez resitele".** Formulare nepotrebuji, jsou skoro
zadarmo a jsou hned videt v provozu.
3. **Viditelnost.** Model, strop v `resolveScope`, vynuceni v jedne ceste ke
ticketum, panel skupiny. Delat to pred hledanim, ne po nem, jinak se hledani
pise dvakrat.
4. **Modal hledani** a serverovy endpoint se strankovanim.
5. **Widget akci** a "Zaciname". Sahne uz jen na hotove formulare.
Postgres kdykoliv mezi tim, nezavisle na ostatnim.
## Co se tim rozbije
- **Zmena `personIds` na `members`** se dotkne sesti mist. Jedno z nich je krok
automatizace `ticket/assign-group`.
- **Strop v `listTickets`** zmeni cisla ve vsech widgetech a v souhrnu tem, kdo
dosud videl celou firmu. To je zamer, ale je to viditelna zmena a chce to rict
dopredu, ne aby se zakaznik lekl, ze prisel o data.
- **Vychozi rozlozeni** se zmeni jen novym uzivatelum. Stavajici uvidi zmenu az po
"Vychozi", coz muze vypadat jako nekonzistence pri predvadeni.
- **Strankovani seznamu ticketu** zmeni chovani dnesniho klientskeho hledani. Musi
jit ruku v ruce se serverovym hledanim, ne pred nim.
## Otevrene otazky
U kazde jde o rozhodnuti, ktere se z modelu neda odvodit.
### A - Co znamena "moje tickety" pro strop
Tri odpovedi, lisi se v tom, kdy clovek o ticket prijde z dohledu:
| Varianta | Dusledek |
| ---------------------------------- | --------------------------------------------------------------- |
| jen prirazene mne | pracovnik zalozi ticket po telefonu, preda ho a hned o nem nevi |
| prirazene plus zalozene mnou | vidi i to, co poslal dal |
| prirazene, zalozene i komentovane | vidi vse, ceho se dotkl |
Ticket dnes nenese, kdo ho zalozil rucne (`automationId` je jen u automatickych),
takze druha a treti varianta znamenaji nove pole na ticketu.
### B - Helpdesk a strop
Firma A posle pozadavek firme B. Zadavatel u firmy A ho vidi pres
`helpdeskSourceId`, i kdyz ticket vlastni firma B. Zadavatel neni resitel a neni
v zadne skupine, takze strop na nej nesedi.
Nabizi se, ze helpdeskovy pohled ma vlastni pravidlo ("vidim, co moje firma
poslala") a strop se na nej nevztahuje. Otazka je, **kdo z firmy A to vidi**:
kazdy clen firmy, nebo jen ten, kdo pozadavek poslal? U firmy o peti lidech je
odpoved jina nez u firmy o padesati.
### C - Hledani v udalostech
Udalosti nesou cele prijate telo webhooku. Hledat v nem znamena hledat v datech,
ktera nikdo nefiltroval, vcetne toho, co tam odesilatel poslal navic. Pro
dohledani hovoru to potreba neni, `callSid` je i externi ID.
Otazka: hledat jen v polich ticketu, nebo i v payloadu udalosti? Druha varianta je
mocnejsi a zaroven znamena, ze se pres hledani da dostat k obsahu, ktery
v rozhrani jinak videt neni.
### D - Kdo smi nastavovat viditelnost
Staci stavajici pravo na spravu lidi a skupin, nebo to ma byt samostatne pravo?
Samostatne dava smysl u firmy, kde HR spravuje lidi, ale o tom, kdo co vidi,
rozhoduje nekdo jiny.
+53
View File
@@ -2,6 +2,59 @@
Nejnovejsi nahore.
## 2026-09-02 - Navrh: pristupny portal a viditelnost
Novy [25-navrh-pristupny-portal.md](25-navrh-pristupny-portal.md). Je to navrh,
ne popis stavu, nic z nej zatim neni naprogramovane.
Vzniklo to z otazky, jak portal priblizit cloveku, ktery ho nikdy nevidel.
Odpoved se rozpadla na pet veci, ktere spolu souvisi vic, nez to vypada:
- **prehled ukazuje jen cisla, zadna slovesa.** Vsech sest widgetu ve vychozi
sade jsou statistiky a seznamy. Navrh pridava "Moje tickety", "Fronta bez
resitele", "Co potrebujete udelat" a "Zaciname"
- **formulare nemaji spolecnou vrstvu.** `inputClass` je nadefinovany na 13
mistech a rozesel se do peti ruznych vzhledu, `Field` je napsany trikrat.
V `components/ui/` neni zadny formularovy prvek
- **hledani neumi to jedine, k cemu je.** Klientsky filtr nehleda v obsahu,
ve vlastnich polich ani v externim ID, takze hovor podle `callSid` se dohledat
neda. Navrh je modal s kriterii, protoze ticket nema pevnou sadu poli
- **viditelnost ticketu se neda omezit.** Pohled `tenant` dostane kazdy, kdo do
firmy patri, `mine` je dobrovolny filtr a ne strop. Navrh vede viditelnost
pres clenstvi (priznak na firme, priznak u kazde skupiny), ne pres role -
role jsou na celou firmu a neumi rict "v jedne sekci vidim vse, v druhe svoje"
- **uloziste neprezije nasazeni.** Overeno v praxi: po dnesnim nasazeni zustaly
ve firme dva tickety, predtim jich byly tisice
Dve veci, ktere stoji za zapamatovani, i kdyby se navrh nikdy nedodelal:
1. **Strop viditelnosti nepatri do hledani, ale do cteni ticketu.** Cesty
k ticketum jsou dnes tri (`listTickets`, `getWorkload`, `getAgentStats`)
a dve z nich filtruji `tickets` primo. Kdyby strop resilo jen hledani,
obejde se widgetem nebo souhrnem.
2. **Pohled a strop nejsou totez.** Pohled je co chci videt, strop je co vubec
smim videt. Dnes existuje jen pohled a klientovi se veri.
Ctyri otevrene otazky jsou v zaveru navrhu: co znamena "moje" pro strop, jak se
strop potka s helpdeskem, jestli hledat i v udalostech a kdo smi viditelnost
nastavovat.
## 2026-09-02 - Vzorova automatizace srovnana s bezici instanci
`seedRealAutomations` v `src/data/automationStore.ts` drzelo starsi podobu stromu
nez ta, ktera na instanci opravdu bezi. Protoze data neprezivaji redeploy, je
tenhle seed jedine misto, kde nastaveni prezije nasazeni - a kdyz se rozejde,
znamena to po kazdem nasazeni stavet strom rucne znovu.
Opsano z bezici instance: spoustec ma sest parametru misto tri (pribylo `result`,
`rating` a `data`, vsechny s cestou do `data`), krok "Zalozit nebo doplnit ticket"
pise do obsahu `{{voicebotId}}, {{data}}` a stav bere z `{{result}}`, a za nim je
podminka nad vysledkem: cokoliv krome "Chybějící informace" ticket zavre, jinak
jde na servicedesk s vysokou prioritou.
Pravidlo, ktere z toho plyne a je i v komentari u funkce: **kdyz se strom na
instanci zmeni, patri ta zmena sem.** Jinak ji dalsi nasazeni zahodi.
## 2026-09-02 - Zalozit NEBO DOPLNIT ticket: doplneni konecne doplnuje
Automatizace mela v kroku "Zalozit nebo doplnit ticket" pole Obsah nastavene na
+43 -2
View File
@@ -978,6 +978,10 @@ function seedDemoAutomations(): void {
*
* Token webhooku se bere z `WEBHOOK_TOKEN_TEST`, aby se adresa po nasazeni
* nemenila a odesilatel ji nemusel prepisovat.
*
* **Opsano z bezici instance, ne vymysleno.** Kdyz se strom na instanci zmeni,
* patri ta zmena sem, jinak ji dalsi nasazeni zahodi. Naposledy srovnano
* 2026-09-02.
*/
function seedRealAutomations(): void {
seed({
@@ -995,6 +999,9 @@ function seedRealAutomations(): void {
{ 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 },
{ id: 'f_mtjqv4qj_1', name: 'result', type: 'string', required: false, path: 'data.result' },
{ id: 'f_mtjqv4zn_2', name: 'rating', type: 'string', required: false, path: 'data.rating' },
{ id: 'f_mtjqvws7_3', name: 'data', type: 'string', required: true, path: 'data' },
],
webhookToken: config.seedWebhookToken || generateWebhookToken(),
},
@@ -1006,12 +1013,46 @@ function seedRealAutomations(): void {
operationId: 'upsert',
inputs: {
externalId: '{{callSid}}',
body: '{{voicebotId}}',
status: '{{status}}',
body: '{{voicebotId}}, {{data}}',
status: '{{result}}',
tags: '{{voicebotId}}',
priority: 'low',
},
},
{
id: 'st_mtjqwey5_4',
kind: 'condition',
fieldId: 'f_mtjqv4qj_1',
operator: 'neq',
value: 'Chybějící informace',
// Vyresene hovory se rovnou zaviraji.
yes: [
{
id: 'st_mtjqx6t1_5',
kind: 'action',
serviceId: 'ticket',
operationId: 'upsert',
inputs: {
externalId: '{{callSid}}',
closed: 'true',
},
},
],
// Chybejici informace jde na servicedesk a s vyssi prioritou.
no: [
{
id: 'st_mtjqxs41_6',
kind: 'action',
serviceId: 'ticket',
operationId: 'upsert',
inputs: {
externalId: '{{callSid}}',
priority: 'high',
groupId: 'grp_servicedesk',
},
},
],
},
],
},
});