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:
co-authored by
Claude Opus 5
parent
1134852bff
commit
72da07debe
@@ -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) |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user