From 72da07debe494504ffdab755825b09b539e3e470 Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Wed, 2 Sep 2026 09:37:46 +0200 Subject: [PATCH] Navrh pristupneho portalu a srovnani vzorove automatizace s instanci MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- documentation/00-pro-programatory.md | 2 + documentation/01-prehled-a-stav.md | 1 + documentation/25-navrh-pristupny-portal.md | 545 +++++++++++++++++++++ documentation/99-zmeny.md | 53 ++ src/data/automationStore.ts | 45 +- 5 files changed, 644 insertions(+), 2 deletions(-) create mode 100644 documentation/25-navrh-pristupny-portal.md diff --git a/documentation/00-pro-programatory.md b/documentation/00-pro-programatory.md index 062d926..ac2d725 100644 --- a/documentation/00-pro-programatory.md +++ b/documentation/00-pro-programatory.md @@ -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) | diff --git a/documentation/01-prehled-a-stav.md b/documentation/01-prehled-a-stav.md index 371c50a..62d6f49 100644 --- a/documentation/01-prehled-a-stav.md +++ b/documentation/01-prehled-a-stav.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 | diff --git a/documentation/25-navrh-pristupny-portal.md b/documentation/25-navrh-pristupny-portal.md new file mode 100644 index 0000000..554db34 --- /dev/null +++ b/documentation/25-navrh-pristupny-portal.md @@ -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. diff --git a/documentation/99-zmeny.md b/documentation/99-zmeny.md index d2a946d..39f494b 100644 --- a/documentation/99-zmeny.md +++ b/documentation/99-zmeny.md @@ -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 diff --git a/src/data/automationStore.ts b/src/data/automationStore.ts index 614cd02..855d662 100644 --- a/src/data/automationStore.ts +++ b/src/data/automationStore.ts @@ -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', + }, + }, + ], + }, ], }, });