Poptavka z webu je ticket, prilohy, udaje provozovatele, kolacovy graf
Provozovatel portalu: firma s priznakem portalOperator (jen jedna, zapnuti odebere ostatnim) a novymi poli contactEmail, contactPhone, website vedle ico, dic, adresy a pravni formy. Verejny GET /api/public/brand vraci jeji udaje a web je bere pres useBrand() na kontaktu, v paticce, O nas, prihlaseni i v titulku; brand.ts je jen zaloha. Poptavka z webu zaklada u provozovatele ticket kanalu form: predmet "Poptavka: tema", telo JSON s poli formulare, tag Poptavka plus tema, zakaznik z formulare, poznamka v logu. Bez provozovatele se jen zaloguje. Prilohy ticketu: formular az 3 soubory po 5 MB, ticket az 10; nahrani, seznam, stazeni a smazani (pravo ticket.comment, strop viditelnosti, poznamky v logu, audit). Soubor jde v JSON jako Base64 a lezi v beznem ulozisti, bez nove zavislosti; strop tela jen na techto cestach. Vlastni widget s kreslenim Graf umi i pocet ticketu se seskupenim jako kolac (PieChart.tsx, ciste SVG, osm barev z tokenu, zbytek jako ostatni). OpenAPI rozdelene na mensi soubory (102 cest, 28 schemat overeno shodnych), 28 novych testu (135 celkem), dokumentace aktualizovana.
This commit is contained in:
@@ -66,7 +66,9 @@ React aplikaci ze slozky `dist/public`.
|
||||
| Monetizace a cena za krok | navrh | popis v 16-monetizace.md, neni naprogramovane |
|
||||
| Helpdesk pro zadavatele | hotovo | pozadavek vidi zadavatel i resitel, kazdy ze sve strany |
|
||||
| Odesilani e-mailu pres SMTP | hotovo | konektor se schrankou firmy, HTML telo s promennymi |
|
||||
| Odesilani e-mailu z formulare | chybi | poptavka se zatim jen loguje |
|
||||
| Poptavka z webu jako ticket | hotovo | `POST /api/contact` zaklada ticket provozovateli portalu |
|
||||
| Prilohy ticketu | hotovo | 5 MB na soubor, 10 na ticket, z webu i z portalu |
|
||||
| Udaje provozovatele na webu | hotovo | `GET /api/public/brand`, `brand.ts` je jen zaloha |
|
||||
| Jeden rezim uloziste | hotovo | `initStores` rozhoduje pro vsechna uloziste naraz |
|
||||
| Zapisy serazene za sebou | hotovo | `withMirror` radi zapisy tehoz zaznamu, audit se oreza |
|
||||
| Worker jako pool | hotovo | ctyri behy naraz nezavisle, tlukot, opakovani jen kdyz ma smysl |
|
||||
@@ -80,7 +82,7 @@ React aplikaci ze slozky `dist/public`.
|
||||
| Struktura podle zasad | hotovo | `index.ts` a `app.ts`, routy a data po slozkach, `connectors/` |
|
||||
| Lint a formatovani v repu | hotovo | eslint a prettier, `npm run lint` cisty bez vyjimek |
|
||||
| Prisny TypeScript | hotovo | `noUncheckedIndexedAccess` v obou tsconfig, zadne `!` |
|
||||
| Testy | castecne | vitest v `tests/`, 8 souboru a 105 testu: prava, tickety, executor, sit, health |
|
||||
| Testy | castecne | vitest v `tests/`, 12 souboru a 130 testu: prava, tickety, prilohy, kontakt, executor, sit, health |
|
||||
|
||||
## Znama omezeni
|
||||
|
||||
@@ -107,9 +109,18 @@ v [19-kapacita-200-firem.md](19-kapacita-200-firem.md).
|
||||
Limity requestu (prihlaseni, kontakt, pozvanky) jsou v pameti jedne instance,
|
||||
stejne jako stream. Pri vice instancich by kazda pocitala zvlast.
|
||||
|
||||
Obsah verejneho webu je ukazkovy. Nazev firmy, reference, tym i cisla jsou
|
||||
vymyslene a pred ostrym pouzitim se musi nahradit. Firemni udaje jsou na jednom
|
||||
miste v `web/src/config/brand.ts`.
|
||||
Obsah verejneho webu je ukazkovy. Reference, tym i cisla jsou vymyslene
|
||||
a pred ostrym pouzitim se musi nahradit. Kontaktni a fakturacni udaje bere web
|
||||
z firmy oznacene jako provozovatel portalu (`GET /api/public/brand`),
|
||||
`web/src/config/brand.ts` je jen staticka zaloha pro pripad, ze provozovatel
|
||||
neni nastaven. Viz [22-znacka-a-design.md](22-znacka-a-design.md).
|
||||
|
||||
Prilohy ticketu lezi v obecnem ulozisti jako base64 uvnitr zaznamu. V rezimu
|
||||
`file` kazda zmena prepise cely `attachment.json`, pro jednotky MB to staci,
|
||||
blob uloziste (S3 nebo `bytea`) je dalsi krok. Viz [14-databaze.md](14-databaze.md).
|
||||
|
||||
Poptavka z webu vznikne jako ticket provozovatele, ale nikdo se o ni nedozvi
|
||||
e-mailem: upozorneni na novou poptavku zatim chybi, resitel ji vidi az v portalu.
|
||||
|
||||
Zivy stream drzi seznam posluchacu v pameti jedne instance. Pri vice instancich
|
||||
by ho musel nahradit sdileny kanal, napriklad Redis pub/sub.
|
||||
@@ -128,8 +139,8 @@ zamer nebo odlozena prace, ne opomenuti:
|
||||
| jeden `package.json` pro server i web | mala aplikace v jednom containeru; workspaces az bude mit kazda strana vlastni build |
|
||||
| logovani `console.*` s prefixem modulu | strukturovany logger (`pino`) zatim neni potreba, prefix `[modul]` staci k dohledani |
|
||||
| zadny soubor CI | lint, typecheck a testy se spousti rucne se svolenim (pravidlo 2) |
|
||||
| testy jen na cast logiky | pokryta prava, tickety, executor, cteni tela a health; routy nastaveni a runtime fronty cekaji |
|
||||
| ctyri soubory nad 500 radku | `AutomationDetail`, `TicketDetail`, `MappingEditor`, `catalog/ticket.ts`; duvod v [03-architektura-a-mapa-kodu.md](03-architektura-a-mapa-kodu.md) |
|
||||
| testy jen na cast logiky | pokryta prava, tickety, prilohy, kontakt, executor, cteni tela a health; routy nastaveni a runtime fronty cekaji |
|
||||
| deset souboru nad 500 radku | tri na webu z duvodu, sedm na serveru ceka na deleni pri nejblizsi praci v nich; seznam v [03-architektura-a-mapa-kodu.md](03-architektura-a-mapa-kodu.md) |
|
||||
|
||||
## Dalsi krok
|
||||
|
||||
|
||||
@@ -64,8 +64,8 @@ viz [99-zmeny.md](99-zmeny.md).
|
||||
| `src/config.ts` | **jedine misto, kde se cte `process.env`**; `serviceBaseUrlOverride(variable)` pro `<SLUZBA>_BASE_URL` |
|
||||
| `src/openapi/index.ts` | `buildOpenApiDocument()`: sklada dokument, `servers` s prefixem proxy |
|
||||
| `src/openapi/helpers.ts` | `crudPaths` a opakujici se parametry, tela a odpovedi |
|
||||
| `src/openapi/components.ts` | schemata a zabezpeceni |
|
||||
| `src/openapi/paths/*.ts` | cesty po routerech: `ops`, `auth`, `dashboard`, `tickets`, `automations`, `settings`, `connectors`, `scripts`, `helpdesk`, `invites`, `admin`, `contact`, `webhook` |
|
||||
| `src/openapi/components/` | schemata a zabezpeceni po domenach (`common`, `auth`, `tickets`, `attachments`, `automations`, `connectors`, `scripts`, `settings`), `index.ts` je sklada |
|
||||
| `src/openapi/paths/*.ts` | cesty po routerech: `ops`, `auth`, `dashboard`, `tickets`, `automations`, `settings`, `connectors`, `scripts`, `helpdesk`, `invites`, `admin`, `contact`, `attachments`, `public`, `webhook` |
|
||||
| `src/types.ts` | typy uzivatele a JWT payloadu |
|
||||
| `src/shared/` | ciste typove moduly API, jediny zdroj typu pro server i web |
|
||||
| `src/middleware/auth.ts` | `requireAuth`, `requireRole`, `requirePlatformAdmin` |
|
||||
@@ -89,6 +89,7 @@ viz [99-zmeny.md](99-zmeny.md).
|
||||
| `src/routes/dashboard/notifications.ts` | upozorneni a pocet otevrenych ticketu |
|
||||
| `src/routes/dashboard/clientCrash.ts` | hlaseni padu portalu, z nej incident |
|
||||
| `src/routes/dashboard/shared.ts` | strankovani: `pageFrom`, `paginate` |
|
||||
| `src/routes/dashboard/attachments.ts` | prilohy ticketu: seznam, nahrani, stazeni, smazani; pravo `ticket.comment` |
|
||||
| `src/routes/ticketActions.ts` | akce nad ticketem vcetne vestavenych, pravo za firmu ticketu |
|
||||
| `src/routes/settings/index.ts` | mount routeru nastaveni |
|
||||
| `src/routes/settings/{tenants,users,roles,people,groups,features,ticketTypes,actions,widgets}.ts` | jedna entita = jeden soubor nad `crudRouter`; `people` a `users` maji vlastni handlery |
|
||||
@@ -99,15 +100,18 @@ viz [99-zmeny.md](99-zmeny.md).
|
||||
| `src/routes/scripts.ts`, `tenantScripts.ts` | skripty konektoru a skripty firmy |
|
||||
| `src/routes/stream.ts` | SSE stream zmen, filtr podle firem uzivatele |
|
||||
| `src/routes/webhook.ts` | verejny prijem dat do automatizace |
|
||||
| `src/routes/contact.ts` | poptavkovy formular z webu |
|
||||
| `src/routes/contact.ts` | poptavkovy formular z webu, zaklada ticket provozovateli portalu |
|
||||
| `src/routes/public.ts` | verejne udaje bez prihlaseni: `GET /api/public/brand` z provozovatele |
|
||||
| `src/routes/bodyLimit.ts` | `jsonLimitFor`, `hasOwnBodyLimit`: strop tela pro routy se soubory v base64 |
|
||||
| `src/ares/client.ts` | klient verejneho API ARES |
|
||||
| `src/data/store/` | tri rezimy uloziste, `withCache`, `withMirror`, `initStores` |
|
||||
| `src/data/snapshot.ts` | atomicky zapis JSONu pro rezim `file` |
|
||||
| `src/data/ticketStore.ts` | fasada nad `src/data/tickets/`, importy zustavaji |
|
||||
| `src/data/tickets/` | `index` (verejne API), `model` (tvar, `toTicket`), `state` (pamet a indexy), `persist` (zapis, `initTickets`), `queries` (seznam, detail, strop viditelnosti), `store` (zapisy: zalozeni, stav, resitel, typ, tagy, skupina, komentar), `intake` (udalost zvenku), `trace` (log prubehu), `stats` (vytizeni a vykon), `seed`, `remap` |
|
||||
| `src/data/tickets/` | `index` (verejne API), `model` (tvar, `toTicket`), `state` (pamet a indexy), `persist` (zapis, `initTickets`), `queries` (seznam, detail, strop viditelnosti), `store` (zapisy: zalozeni, stav, resitel, typ, tagy, skupina, komentar, poznamka o priloze), `intake` (udalost zvenku), `trace` (log prubehu), `stats` (vytizeni a vykon), `seed`, `remap` |
|
||||
| `src/data/people.ts` | resitele jako pohled na clenstvi uctu (`personView`), skupiny |
|
||||
| `src/data/migratePeople.ts` | jednorazovy prevod starych zaznamu resitelu `ppl_` na ucty |
|
||||
| `src/data/tenants.ts` | firmy, ktere portal pouzivaji, vcetne udaju z ARES |
|
||||
| `src/data/tenants.ts` | firmy, ktere portal pouzivaji, vcetne udaju z ARES; `operatorTenant`, `clearOtherOperators` |
|
||||
| `src/data/attachments.ts` | prilohy ticketu: kontrola davky, zapis s obsahem v base64, cteni, mazani |
|
||||
| `src/data/access.ts` | kdo co vidi - jedno misto pro cely portal |
|
||||
| `src/data/widgets.ts` | katalog widgetu prehledu |
|
||||
| `src/data/dashboardLayouts.ts` | rozlozeni dashboardu za dvojici uzivatel a firma |
|
||||
@@ -135,7 +139,8 @@ viz [99-zmeny.md](99-zmeny.md).
|
||||
| `web/src/main.tsx` | vstupni bod, `basename` routeru podle prefixu proxy |
|
||||
| `web/src/App.tsx` | routovani, portal se nacita lazy |
|
||||
| `web/src/index.css` | design tokeny a vlastni utility Tailwindu |
|
||||
| `web/src/config/brand.ts` | vsechny firemni udaje na jednom miste |
|
||||
| `web/src/config/brand.ts` | nazev, claim a staticka zaloha kontaktu; kontakty bere web z provozovatele pres `useBrand` |
|
||||
| `web/src/lib/files.ts` | soubor na base64, kontrola poctu a velikosti, limity stejne jako na serveru |
|
||||
| `web/src/lib/api.ts` | fetch wrapper, sprava tokenu, skladani adres, `auth:expired` na 401 |
|
||||
| `web/src/lib/eventStream.ts` | cteni SSE streamu pres fetch |
|
||||
| `web/src/lib/collections.tsx` | klientsky sklad ciselniku za firmu, opravovany z udalosti |
|
||||
@@ -151,16 +156,18 @@ viz [99-zmeny.md](99-zmeny.md).
|
||||
| `web/src/hooks/useSyncFromSource.ts` | prevzeti dat ze zdroje do rozepsaneho stavu pri vykresleni, ne v effectu |
|
||||
| `web/src/hooks/useMediaQuery.ts` | sirka obrazovky pres `useSyncExternalStore` |
|
||||
| `web/src/hooks/usePageMeta.ts` | titulek a popis stranky |
|
||||
| `web/src/hooks/useBrand.ts` | udaje provozovatele z `/api/public/brand` nad zalohou `brand.ts`, jeden sdileny dotaz |
|
||||
| `web/src/types/` | fasada nad `src/shared` (alias `@shared/*`), zadne vlastni typy API |
|
||||
| `web/src/components/ui/` | zakladni prvky: `Badge`, `Button`, `Card`, `Chip`, `Modal`, `Section`, `Spinner`, ... |
|
||||
| `web/src/components/ui/Table.tsx` | `Table`, `TableHead`, `Th`, `TableRow`, `Td`: jedna tabulka seznamu pro `EntityAdmin`, `InvitePanel`, `People`, `AuditView` |
|
||||
| `web/src/components/ui/ServiceIcon.tsx` | ikona sluzby podle klice z katalogu, misto `const Icon = serviceIcon()` v JSX |
|
||||
| `web/src/components/ui/form/` | `Field`, `Input`, `Select`, `Textarea`, `controlClass`: jedna sada trid |
|
||||
| `web/src/components/ui/form/` | `Field`, `Input`, `Select`, `Textarea`, `FilePicker`, `controlClass`: jedna sada trid |
|
||||
| `web/src/components/dashboard/` | shell portalu, dlazdice, graf, stream, `TicketCard` |
|
||||
| `web/src/components/dashboard/EntityAdmin.tsx`, `EntityForm.tsx` | sprava jedne entity: tabulka a formular v modalu |
|
||||
| `web/src/components/dashboard/flow/` | strom akci: `FlowCanvas` a karty `ActionCard`, `ConditionCard`, `ForeachCard`, `StepControls`; k tomu `TriggerConfig`, `SampleBody`, `ModelTree`, `WebhookCalls`, `MappingEditor` |
|
||||
| `web/src/components/dashboard/scripts/` | `TestPanel` (zkusebni spusteni) a `CodeEditor` (uprava kodu) pro stranku Skripty |
|
||||
| `web/src/components/dashboard/settings/` | `FeaturesAdmin` (zalozky a limity), `AuditView`, `types` |
|
||||
| `web/src/components/dashboard/settings/` | `TenantsAdmin` (firmy, provozovatel, kontakt), `FeaturesAdmin` (zalozky a limity), `AuditView`, `types` |
|
||||
| `web/src/components/dashboard/TicketAttachments.tsx`, `TicketAssignPanel.tsx` | sekce priloh a panel resitele v detailu ticketu |
|
||||
| `web/src/components/dashboard/widgets/` | `WidgetCard`, `WidgetPicker`, `CustomWidget`, `EditBar` (lista uprav rozlozeni) |
|
||||
| `web/src/components/dashboard/TicketTrace.tsx` | log ticketu jako strom |
|
||||
| `web/src/components/dashboard/TicketWorkload.tsx` | prehled, kdo co ma u sebe |
|
||||
@@ -171,15 +178,25 @@ viz [99-zmeny.md](99-zmeny.md).
|
||||
### Soubory nad 500 radku
|
||||
|
||||
Zasada rika, ze soubor nad 500 radku je signal k rozdeleni. Po rozdeleni
|
||||
zustavaji ctyri, kazdy z duvodu:
|
||||
zustavaji tri, kazdy z duvodu (`TicketDetail` sel pod hranici vyclenenim
|
||||
`TicketAssignPanel` a `TicketAttachments`, `Settings` vyclenenim
|
||||
`TenantsAdmin`):
|
||||
|
||||
| Soubor | Proc zustava |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `web/src/pages/dashboard/AutomationDetail.tsx` | stranka drzi stav stromu a ukladani; casti bez stavu uz jsou ve `flow/` |
|
||||
| `web/src/pages/dashboard/TicketDetail.tsx` | detail sklada sest komponent, zbytek je stav a odeslani akci |
|
||||
| `web/src/components/dashboard/flow/MappingEditor.tsx` | dva rezimy editoru nad jednim stavem, deleni by stav zdvojilo |
|
||||
| `src/data/services/catalog/ticket.ts` | jedna sluzba s nejvic operacemi; deleni jedne sluzby do dvou souboru by rozbilo "jedna vec v jednom souboru" |
|
||||
|
||||
Na serveru zustava dalsich sedm, ktere strukturalni pruchod nedelil, protoze
|
||||
nebyly v zadani a kazdy je jeden souvisly modul: `runtime/builtinSteps.ts`
|
||||
(929, vestavene kroky; kandidat na slozku `runtime/steps/` po kroku),
|
||||
`mcp/client.ts` (884, protokol MCP; kandidat na oddeleni handshake a volani
|
||||
nastroju), `runtime/executor.ts` (779; kandidat na vycleneni vyhodnoceni
|
||||
podminek), `routes/connectors.ts` (677; kandidat na `routes/connectors/`),
|
||||
`routes/ticketActions.ts` (599), `routes/widgetData.ts` (587), `mcp/auth.ts`
|
||||
(527). Deli se pri nejblizsi praci v nich, ne naraz.
|
||||
|
||||
Dalsi velke soubory (`builtinSteps.ts`, `mcp/client.ts`, `executor.ts`,
|
||||
`routes/connectors.ts`) jsou kandidati na priste, az se do nich bude sahat.
|
||||
|
||||
|
||||
+76
-1
@@ -14,7 +14,8 @@ Verejne:
|
||||
| GET | `/docs` | Swagger UI |
|
||||
| GET | `/openapi.json` | OpenAPI definice |
|
||||
| POST | `/api/auth/login` | prihlaseni, vraci JWT |
|
||||
| POST | `/api/contact` | poptavka z webu |
|
||||
| POST | `/api/contact` | poptavka z webu, vznikne ticket provozovatele |
|
||||
| GET | `/api/public/brand` | udaje provozovatele portalu pro web |
|
||||
| POST | `/webhook/:token` | prijem dat do automatizace |
|
||||
| POST | `/webhook/ticket/:token` | prijem udalosti do ticketu |
|
||||
| GET | `/webhook/ticket/:token` | napoveda k prijmu |
|
||||
@@ -51,6 +52,10 @@ Vyzaduji `Authorization: Bearer <token>`:
|
||||
| POST | `/api/dashboard/tickets/:id/claim` |
|
||||
| GET | `/api/dashboard/tickets/:id/actions` |
|
||||
| POST | `/api/dashboard/tickets/:id/actions/:actionId` |
|
||||
| GET | `/api/dashboard/tickets/:id/attachments` |
|
||||
| POST | `/api/dashboard/tickets/:id/attachments` |
|
||||
| GET | `/api/dashboard/tickets/:id/attachments/:attachmentId/content` |
|
||||
| DELETE | `/api/dashboard/tickets/:id/attachments/:attachmentId` |
|
||||
| GET | `/api/dashboard/invites` |
|
||||
| POST | `/api/dashboard/invites` |
|
||||
| DELETE | `/api/dashboard/invites/:id` |
|
||||
@@ -142,6 +147,75 @@ pozvanky 5 za 15 minut. Pocita se podle adresy klienta, proto ma Express
|
||||
Kazdy asynchronni handler je obaleny (`safeRouter` v `src/middleware/asyncHandler.ts`).
|
||||
Odmitnuta promise je 500 s logem, ne pad procesu.
|
||||
|
||||
## Strop tela requestu
|
||||
|
||||
Globalni `express.json` v `src/app.ts` ma 256 kB. To staci na formulare
|
||||
a stromy automatizaci, ne na soubory. Dve cesty prijimaji soubory v base64
|
||||
a maji **vlastni** `express.json` s vetsim stropem; globalni parser je
|
||||
preskakuje (`hasOwnBodyLimit` v `src/routes/bodyLimit.ts`), jinak by telo
|
||||
odmitl driv, nez se k nemu router dostane.
|
||||
|
||||
| Cesta | Strop |
|
||||
| ---------------------------------------- | ------------------------------------------- |
|
||||
| `POST /api/contact` | `jsonLimitFor(3, 5 MB)`, tj. 3 soubory |
|
||||
| `POST /api/dashboard/tickets/:id/attachments` | `jsonLimitFor(10, 5 MB)`, tj. 10 souboru |
|
||||
|
||||
`jsonLimitFor(count, maxBytes)` pocita `count * maxBytes * 4/3` (base64) plus
|
||||
64 kB rezervy na zbytek JSONu. Vypocet je na jednom miste, aby formular
|
||||
a prilohy pocitaly stejne. U kontaktu bezi limit pokusu **pred** parserem
|
||||
tela: kdo uz pokusy vycerpal, nema server nutit cist megabajty.
|
||||
|
||||
## Poptavka z webu
|
||||
|
||||
`POST /api/contact` je verejny, 5 poptavek za hodinu z jedne adresy. Telo:
|
||||
`name`, `email`, `topic` (`automatizace`, `voicebot`, `integrace`,
|
||||
`dashboard`, `podpora`, `jine`), `message`, nepovinne `company`, `phone`
|
||||
a `attachments` (nejvys 3, kazda `{ name, mime?, content }` s obsahem
|
||||
v base64).
|
||||
|
||||
Poptavka vznikne jako **ticket firmy, ktera je provozovatelem portalu**
|
||||
(`Tenant.portalOperator`, viz [07-firmy-a-prava.md](07-firmy-a-prava.md)):
|
||||
kanal `form`, predmet `Poptávka: <tema>`, telo je JSON formulare, tagy
|
||||
`Poptávka` a tema, `externalSource` `web-form`. Kdyz zadna firma
|
||||
provozovatelem neni, poptavka se jen zaloguje (warn). Odpoved je v obou
|
||||
pripadech 202 - zvenku nema byt poznat, jak je portal nastaveny. Podrobnosti
|
||||
v [06-tickety.md](06-tickety.md).
|
||||
|
||||
## Udaje provozovatele
|
||||
|
||||
`GET /api/public/brand` je verejny, bez limitu pokusu (cte z kopie firem
|
||||
v pameti, je levnejsi nez health) a s `Cache-Control: public, max-age=60`.
|
||||
Vraci `name`, `legalName`, `ico`, `dic`, `address`, `legalForm`, `email`,
|
||||
`phone`, `website` provozovatele portalu; bez provozovatele same `null`
|
||||
a porad 200, aby web umel rict "neni nastaveno" misto padu. Web ho cte
|
||||
pres `useBrand`, viz [22-znacka-a-design.md](22-znacka-a-design.md).
|
||||
|
||||
## Prilohy ticketu
|
||||
|
||||
Model je v [06-tickety.md](06-tickety.md). Prilohy nejsou pole ticketu, maji
|
||||
vlastni cesty pod `/api/dashboard/tickets/:id/attachments`:
|
||||
|
||||
| Volani | Co se stane |
|
||||
| ---------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| `GET attachments` | seznam bez obsahu (`id`, `name`, `mime`, `size`, `uploadedBy`, `createdAt`) |
|
||||
| `POST attachments` | telo `{ files: [{ name, mime?, content }] }`, obsah base64; vraci 201 a `items` |
|
||||
| `GET attachments/:attachmentId/content` | binarni obsah s `Content-Type` a `Content-Disposition` (nazev v RFC 5987) |
|
||||
| `DELETE attachments/:attachmentId` | 204 |
|
||||
|
||||
Limity: 5 MB na soubor po dekodovani, 10 priloh na ticket, nazev bez cesty
|
||||
a ridicich znaku, nejvys 200 znaku. Kontrola bezi nad **celou davkou** pred
|
||||
prvnim zapisem: kdyz neprojde treti soubor, neulozi se ani prvni dva.
|
||||
|
||||
Ticket se hleda stejne jako u detailu (`visibleTicketOrDeny`): cizi nebo nad
|
||||
strop viditelnosti je 404. Zapis a mazani chce `ticket.comment` za firmu
|
||||
ticketu - priloha je jen dalsi zprava k ticketu. Kazda zmena zapise radek do
|
||||
logu ticketu, posle `ticket.updated` a jde do auditu jako
|
||||
`ticket.attachment.add` / `ticket.attachment.remove`.
|
||||
|
||||
Stazeni chce hlavicku `Authorization`, obycejny odkaz `<a href>` ji neposle.
|
||||
Portal proto stahuje pres fetch (`apiBlob` v `web/src/lib/api.ts`) a docasny
|
||||
odkaz na blob.
|
||||
|
||||
## Autentizace
|
||||
|
||||
Hesla se hashuji bcryptem, plaintext se nikde neuklada. Login vraci JWT
|
||||
@@ -401,6 +475,7 @@ Pravo se vzdy pta **za firmu zaznamu**, ne za prepnutou firmu. Cizi firma je
|
||||
| automatizace create, update, delete, regenerate | `automation.edit` |
|
||||
| `/services`, `/connectors/services` | clenstvi ve firme |
|
||||
| assign, status, comment, claim na ticketu | prava vestavene akce za firmu ticketu plus strop viditelnosti |
|
||||
| prilohy ticketu (POST, DELETE) | `ticket.comment` za firmu ticketu plus strop viditelnosti |
|
||||
| `/api/admin/impersonate*` | `impersonate` |
|
||||
| `/api/admin/audit` | `audit.view` |
|
||||
| `/storage`, `/scripts` s cestami na serveru | cesty jen spravci platformy, ostatni dostanou odpoved bez nich |
|
||||
|
||||
@@ -231,6 +231,96 @@ do korene a zaloguje - ztratit radek logu je horsi nez ho ukazat spatne zanoreny
|
||||
Komentare jsou taky zaznamy logu (`kind: 'note'`). Diky tomu je vsechno na jedne
|
||||
casove ose a nemusi se nikde skladat dohromady dva ruzne seznamy.
|
||||
|
||||
Prilohy do logu zapisuji taky `note`: `Příloha: nazev (velikost)` pri nahrani,
|
||||
`Příloha odebrána: nazev` pri smazani (`noteAttachment`
|
||||
v `src/data/tickets/store.ts`). Radek v logu je jedine, co ticket o priloze
|
||||
vi - soubor sam lezi jinde, viz nize.
|
||||
|
||||
## Prilohy
|
||||
|
||||
`src/data/attachments.ts`, typ `Attachment` v `src/shared/attachments.ts`
|
||||
|
||||
Priloha je soubor, ktery prisel s poptavkou z webu nebo ho nekdo pripojil
|
||||
v detailu ticketu. **Neni to pole ticketu.** `Ticket` o prilohach nic nenese,
|
||||
lezi ve vlastni kolekci `attachment` obecneho uloziste a vazou se pres
|
||||
`ticketId` a `tenantId`:
|
||||
|
||||
```ts
|
||||
interface Attachment {
|
||||
id: string; // att_xxxxxxxx
|
||||
tenantId: string; // firma ticketu, hranice viditelnosti
|
||||
ticketId: string;
|
||||
name: string; // bez cesty a ridicich znaku, nejvys 200 znaku
|
||||
mime: string; // neznamy = application/octet-stream
|
||||
size: number; // bajty po dekodovani
|
||||
uploadedBy: string | null; // ucet; null = prisla z verejneho formulare
|
||||
createdAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
Proc vlastni kolekce a ne pole na ticketu:
|
||||
|
||||
| Duvod | Co by se stalo s polem na ticketu |
|
||||
| --------------------------- | -------------------------------------------------------------- |
|
||||
| obsah je velky | kazde cteni seznamu ticketu by taha megabajty base64 |
|
||||
| meni se nezavisle | nahrani souboru by prepisovalo cely ticket a soutezilo s komentari |
|
||||
| seznam se vraci bez obsahu | obsah jde jen pres `/attachments/:id/content`, ne v detailu |
|
||||
|
||||
Obsah se uklada jako **base64 uvnitr zaznamu**. Zadna nova zavislost (multer,
|
||||
S3 klient): prototyp ma ulozit jednotky MB a obecne uloziste to zvladne.
|
||||
Cenou je, ze v rezimu `file` kazda zmena prepise cely `attachment.json`.
|
||||
Az to zacne byt znat, presune se obsah do blob uloziste; rozhrani modulu
|
||||
zustane, zmeni se jen odkud `getAttachment` cte `content`.
|
||||
Viz [14-databaze.md](14-databaze.md).
|
||||
|
||||
Limity: 5 MB na soubor (`MAX_ATTACHMENT_BYTES`), 10 na ticket
|
||||
(`MAX_ATTACHMENTS_PER_TICKET`). `checkUploads` je cista funkce nad celou
|
||||
davkou: kdyz neprojde treti soubor, neulozi se ani prvni dva, jinak by klient
|
||||
nevedel, co uz tam je. Kontaktni formular ji vola **pred** zalozenim ticketu,
|
||||
aby po chybe nezustala poptavka bez souboru, ktere k ni patrily.
|
||||
|
||||
Nazev projde `sanitizeName`: bere se jen cast za poslednim lomitkem,
|
||||
ridici znaky se vyhodi. Klient posila, co chce - `../../etc/passwd` nebo
|
||||
nazev s novym radkem, ktery by rozbil hlavicku `Content-Disposition`.
|
||||
|
||||
Kazde nahrani a smazani zapise radek do logu ticketu, posune `updatedAt`
|
||||
a posle `ticket.updated`, aby se detail v portalu prekreslil stejne jako po
|
||||
komentari. Pravo je `ticket.comment` za firmu ticketu: priloha je jen dalsi
|
||||
zprava k ticketu. API je v [04-api.md](04-api.md), portal ma sekci
|
||||
`TicketAttachments` v detailu ticketu.
|
||||
|
||||
Mazani ticketu dnes neexistuje, proto tu neni kaskada. Az pribude, patri
|
||||
do `attachments.ts` `removeAttachmentsOfTicket(ticketId)`.
|
||||
|
||||
## Poptavka z webu
|
||||
|
||||
`src/routes/contact.ts`
|
||||
|
||||
Kontaktni formular na verejnem webu je **kanal `form`** jako kazdy jiny:
|
||||
`POST /api/contact` zalozi ticket firme, ktera je provozovatelem portalu
|
||||
(`operatorTenant`, viz [07-firmy-a-prava.md](07-firmy-a-prava.md)). Driv se
|
||||
poptavka jen zalogovala a nikdo ji nevidel.
|
||||
|
||||
| Pole ticketu | Hodnota |
|
||||
| ---------------- | ------------------------------------------------------------- |
|
||||
| `channel` | `form` |
|
||||
| `subject` | `Poptávka: <tema>` (automatizace, voicebot, integrace, dashboard, podpora, jine) |
|
||||
| `body` | JSON formulare: `name`, `email`, `company`, `phone`, `topic`, `message`, `receivedAt` |
|
||||
| `tags` | `Poptávka` a tema |
|
||||
| `customer` | `id` null, `company` a `contact` z formulare, `reply` = e-mail |
|
||||
| `externalSource` | `web-form`, `externalId` null |
|
||||
| `createdById` | null |
|
||||
| `trace` | `note` "Poptávka z webového formuláře" s odesilatelem a tematem |
|
||||
|
||||
Telo je JSON, ne veta, zamerne: `TicketBody` ho ukaze jako tabulku klic
|
||||
a hodnota (viz vyse "Obsah se na detailu cte") a zadne pole formulare se
|
||||
neztrati v prose. Prilohy z formulare (nejvys 3) se ulozi pres
|
||||
`addAttachments` s `uploadedBy` null.
|
||||
|
||||
Bez provozovatele se poptavka jen zaloguje (warn) a odpoved je porad 202:
|
||||
zvenku nema byt poznat, jak je portal nastaveny. Upozorneni e-mailem na novou
|
||||
poptavku zatim neni, resitel ji vidi az v seznamu ticketu.
|
||||
|
||||
## Opakovana udalost
|
||||
|
||||
Odesilatele umi poslat stejnou zpravu i osmdesatkrat za minutu. Kdyz prijde
|
||||
@@ -509,5 +599,6 @@ to rozhodnout vedome, ne omylem. Varianty od nejlevnejsi:
|
||||
| `inputs` u zbylych konektoru | zatim ticket, kanaly, CRM a AI, ostatni maji jen napovedu |
|
||||
| Napojeni logu na beh | `automationId` je odkaz, historie behu ale neexistuje |
|
||||
| Odpoved zakaznikovi z detailu | akce `send` u kanalu se z portalu nevola |
|
||||
| Upozorneni na poptavku z webu | ticket vznikne, ale nikomu neprijde e-mail |
|
||||
| SLA a eskalace | zadne lhuty, `capacity` je jen orientacni |
|
||||
| Databaze | data v pameti, restart je vrati na vychozi sadu |
|
||||
|
||||
@@ -228,6 +228,8 @@ a drzi se vsude, kde se neco zaklada:
|
||||
| automatizace | `automation.edit` za firmu automatizace | `src/routes/dashboard/automations.ts` |
|
||||
| stav incidentu | `incident.manage` za firmu incidentu, platformni jen spravce platformy | `src/routes/dashboard/incidents.ts` |
|
||||
| akce nad ticketem | pravo akce za firmu ticketu a strop viditelnosti | `src/routes/ticketActions.ts` |
|
||||
| priloha ticketu | `ticket.comment` za firmu ticketu a strop viditelnosti | `src/routes/dashboard/attachments.ts` |
|
||||
| provozovatel portalu | jen spravce platformy, prave jedna firma | `src/routes/settings/tenants.ts` |
|
||||
|
||||
Spravce firmy s `user.manage` ma **jen svou firmu**: nenastavi `platformAdmin`,
|
||||
neprida cizi clenstvi, nesahne na spravce platformy a nesmaze cloveka, ktery
|
||||
@@ -261,6 +263,31 @@ firmy vrati `existingTenantId` misto tlacitka Zalozit.
|
||||
Od te chvile je to na spravci firmy: skupiny, vedouci, clenove, pozvanky.
|
||||
Spravce platformy do firmy nesaha, pokud nemusi.
|
||||
|
||||
### Provozovatel portalu
|
||||
|
||||
Jedna z firem je **provozovatel portalu** (`Tenant.portalOperator`). Je to
|
||||
ta, ktera portal provozuje pro ostatni: chodi ji poptavky z verejneho
|
||||
kontaktniho formulare jako tickety (kanal `form`, viz
|
||||
[06-tickety.md](06-tickety.md)) a verejny web z ni bere kontaktni
|
||||
a fakturacni udaje (`GET /api/public/brand`, viz
|
||||
[22-znacka-a-design.md](22-znacka-a-design.md)).
|
||||
|
||||
Nastavuje ho **jen spravce platformy** v Nastaveni, Firmy - je to totez
|
||||
rozhodnuti jako zalozeni firmy, nase pravo, ne zakaznicke. Provozovatel je
|
||||
**prave jeden**: kdyz priznak dostane dalsi firma, ostatnim se sunda
|
||||
(`clearOtherOperators` v `src/data/tenants.ts`, volane z `afterWrite`
|
||||
CRUD firem). Dve firmy s poptavkami by znamenaly, ze se ticket zalozi jen
|
||||
jedne a nikdo nevi ktere. Zmenene firmy se ohlasi do streamu jako
|
||||
`tenant.updated`, aby si portal opravil sklad ciselniku.
|
||||
|
||||
Vypnuta firma provozovatelem neni, i kdyz priznak ma (`operatorTenant`).
|
||||
Bez provozovatele se poptavka jen zaloguje a web ukaze statickou zalohu.
|
||||
|
||||
K tomu firma nese kontaktni udaje pro web: `contactEmail`, `contactPhone`
|
||||
a `website`. Vyplnuji se u provozovatele; u ostatnich firem nic neznamenaji,
|
||||
ale schema je nezakazuje. Prazdny retezec z formulare se uklada jako `null`
|
||||
(`emptyToNull` v `src/routes/settings/tenants.ts`).
|
||||
|
||||
## Co chybi
|
||||
|
||||
| Chybi | Poznamka |
|
||||
|
||||
@@ -126,6 +126,23 @@ Tickety navic slucuji vic zmen v jednom tiku do jednoho zapisu
|
||||
nebo nejvys jednou za minutu. Prvni verze mazala jen radky platformy
|
||||
(`tenantId: null`) a audit firem rostl donekonecna.
|
||||
|
||||
**Prilohy ticketu** (`kind: 'attachment'`, `src/data/attachments.ts`) jdou
|
||||
primo pres `defineStore` bez obalky: nectou se pri kazdem requestu a nemeni
|
||||
se v pameti, kazde volani jde do uloziste. Zaznam nese vedle metadat
|
||||
(`ticketId`, `name`, `mime`, `size`, `uploadedBy`) i **obsah souboru
|
||||
v base64**. V Postgresu je to radek v `records` jako u kazde jine entity,
|
||||
v rezimu `file` soubor `DATA_DIR/attachment.json`.
|
||||
|
||||
Velikost je tu jina nez u ostatnich kolekci: jeden zaznam ma az 5 MB
|
||||
(po base64 skoro 7), ticket jich muze mit deset. V rezimu `file` kazda
|
||||
zmena prepise **cely** `attachment.json`, takze s kazdou prilohou roste
|
||||
cena zapisu vsech ostatnich. Pro jednotky MB v prototypu to staci a zadna
|
||||
nova zavislost nebyla potreba. Dalsi krok je presun obsahu do blob uloziste
|
||||
(S3, nebo tabulka s `bytea`), az to zacne byt znat; rozhrani modulu
|
||||
zustane, zmeni se jen odkud `getAttachment` cte `content`. Seznam priloh
|
||||
obsah nikdy nevraci (`toPublic`), aby se megabajty netahaly pri kazdem
|
||||
otevreni detailu.
|
||||
|
||||
### Klic mimo databazi
|
||||
|
||||
Bez `SECRETS_KEY` si aplikace v rezimu `file` vygeneruje klic do
|
||||
@@ -282,3 +299,4 @@ Proti Postgresu 16 v kontejneru:
|
||||
| Sbernice udalosti pres LISTEN/NOTIFY | dnes `EventEmitter` v pameti jedne instance |
|
||||
| Vymena klice (rotace) | `v` je pripravene, prevod dat napsany neni |
|
||||
| Retence a partitionovani | az u tabulek behu, viz dokument 10 |
|
||||
| Blob uloziste pro prilohy | obsah je base64 v zaznamu, v rezimu `file` se prepisuje cely `attachment.json` |
|
||||
|
||||
@@ -87,7 +87,7 @@ Puvodni soubory zustavaji jako fasady (`ticketStore.ts`, `automationStore.ts`,
|
||||
| `tickets/state.ts` | pole ticketu, indexy podle ID a externiho ID, log, udalosti, citace ID |
|
||||
| `tickets/persist.ts` | `persist`, `touch` (`updatedAt` a zapis v jednom), `initTickets` |
|
||||
| `tickets/queries.ts` | `listTickets` s `TicketFilter`, `getTicket`, `findTicket`, `findByExternalId`, `ticketWithinVisibility` |
|
||||
| `tickets/store.ts` | zapisy: `createTicket`, `updateTicketStatus`, `assignTicket`, `setTicketType`, `setTicketTags`, `assignTicketGroup`, `claimTicket`, `addComment` |
|
||||
| `tickets/store.ts` | zapisy: `createTicket`, `updateTicketStatus`, `assignTicket`, `setTicketType`, `setTicketTags`, `assignTicketGroup`, `claimTicket`, `addComment`, `noteAttachment` (radek v logu a `ticket.updated` po zmene prilohy) |
|
||||
| `tickets/intake.ts` | `intakeEvent`: udalost zvenku se stane ticketem nebo se navesi |
|
||||
| `tickets/trace.ts` | `appendTrace`, `flattenTrace`, `lastTraceId`, `describePayload`: log prubehu |
|
||||
| `tickets/stats.ts` | `getWorkload`, `getAgentStats` |
|
||||
@@ -105,6 +105,15 @@ Puvodni soubory zustavaji jako fasady (`ticketStore.ts`, `automationStore.ts`,
|
||||
| `services/catalog/index.ts` | `services` a `serviceCategories` slozene ze skupin; poradi tady je poradi v nabidce |
|
||||
| `services/catalog/<skupina>.ts` | staticky zapis sluzeb jedne skupiny: `triggers`, `incident`, `ticket`, `crm`, `finance`, `logistics`, `email`, `messaging`, `social`, `office`, `analytics`, `ai`, `mcp`, `tools`, `polstryn` |
|
||||
| `findByIntakeToken(token)` | `src/data/tenants.ts` | Firma podle tokenu příjmu. Určuje i to, v jakém rozsahu je externí ID unikátní. |
|
||||
| `operatorTenant()` | `src/data/tenants.ts` | Zapnuta firma s priznakem provozovatele portalu. Komu chodi poptavky z webu a odkud web bere udaje. `undefined` = neni nastaven. |
|
||||
| `clearOtherOperators(keepId)` | `src/data/tenants.ts` | Sunda priznak provozovatele vsem ostatnim firmam a ohlasi je do streamu. Vola `afterWrite` CRUD firem, provozovatel je vzdy jen jeden. |
|
||||
| `checkUploads(uploads, existingCount)` | `src/data/attachments.ts` | Cista kontrola davky souboru: pocet, base64, velikost, nazev. Volat pred zapisem, kontakt ji vola pred zalozenim ticketu. |
|
||||
| `addAttachments(ticket, uploads, uploadedBy)` | `src/data/attachments.ts` | Ulozi soubory k ticketu a kazdy zapise do logu. Cela davka projde, nebo nic. |
|
||||
| `listAttachments`, `getAttachment`, `removeAttachment` | `src/data/attachments.ts` | Seznam bez obsahu, jedna priloha s obsahem, smazani se zapisem do logu. Vzdy s `ticketId` a `tenantIds`, cizi je jako neexistujici. |
|
||||
| `sanitizeName`, `normalizeMime`, `decodeBase64`, `formatBytes` | `src/data/attachments.ts` | Nazev bez cesty a ridicich znaku, typ obsahu do hlavicky, prisne base64, velikost pro cloveka. |
|
||||
| `jsonLimitFor(count, maxBytes)`, `hasOwnBodyLimit(path)` | `src/routes/bodyLimit.ts` | Strop tela pro routy se soubory v base64 a seznam cest, ktere globalni `express.json` preskakuje. |
|
||||
| `visibleTicketOrDeny(req, res)` | `src/routes/ticketActions.ts` | Ticket z `:id` pres strop viditelnosti za firmu ticketu, jinak 404. Pouzivaji vestavene akce i prilohy. |
|
||||
| `brandOfOperator()` | `src/routes/public.ts` | `PublicBrand` z provozovatele portalu, same `null` bez nej. |
|
||||
| `refreshCaches()`, `refreshEntity(kind)` | `src/data/bootstrap.ts` | Obnoví všechny kopie v paměti, nebo jen jednu entitu. Route nastavení volá `bootstrapDataRefresh(route)` v `src/data/refresh.ts`, která vybere tu jednu. |
|
||||
| `bootstrapData({databaseReady})` | `src/data/bootstrap.ts` | Seznam všech entit a provozních dat. **Nová entita se přidává tady**, ne rozesetě po modulech. |
|
||||
|
||||
@@ -145,6 +154,9 @@ Viz [11-skripty-konektoru.md](11-skripty-konektoru.md).
|
||||
| `CustomWidgetCard` | `components/dashboard/widgets/CustomWidget.tsx` | Vykreslí widget, jehož data počítá server: číslo, pruhy, tabulka výkonu, časová řada, seznam, data z konektoru. |
|
||||
| `TicketTable` | `components/dashboard/TicketTable.tsx` | Tabulka ticketů pro všechna místa. Na mobilu se místo posouvání do strany kreslí karty. |
|
||||
| `TicketEvents` | `components/dashboard/TicketEvents.tsx` | Příchozí události ticketu včetně celého přijatého JSONu. |
|
||||
| `TicketAttachments` | `components/dashboard/TicketAttachments.tsx` | Sekce priloh v detailu ticketu: seznam, stazeni pres fetch s tokenem, nahrani, smazani. Vlastni dotaz, obnovi se z `ticket.updated`. |
|
||||
| `TicketAssignPanel` | `components/dashboard/TicketAssignPanel.tsx` | Panel resitele a skupiny v detailu ticketu, ciselniky si bere sam. Vyclenen z `TicketDetail`. |
|
||||
| `TenantsAdmin` | `components/dashboard/settings/TenantsAdmin.tsx` | Sprava firem v Nastaveni vcetne priznaku provozovatele a kontaktnich udaju. Vyclenena ze `Settings.tsx`. |
|
||||
| `ViewSwitch` | `components/dashboard/ViewSwitch.tsx` | Přepínač tabulka nebo dlaždice. Používají ho všechny seznamy. |
|
||||
| `FlowCanvas` s `start` | `components/dashboard/flow/FlowCanvas.tsx` | Tentýž strom kroků i bez spouštěče - pro tělo akce, které spouští člověk. |
|
||||
| `MappingEditor` | `components/dashboard/flow/MappingEditor.tsx` | Editor transformací v obou režimech (pole na pole, JSON). |
|
||||
@@ -159,6 +171,10 @@ Viz [11-skripty-konektoru.md](11-skripty-konektoru.md).
|
||||
| `priorities`, `priorityLabel` | `lib/options.ts` | Pevné číselníky. Stavy a kanály se berou ze serveru (`/widget-data/options`). |
|
||||
| `plural(count, forms)` | `lib/format.ts` | Skloňování počtu (1 ticket, 2 tickety, 5 ticketů). |
|
||||
| `Field`, `Input`, `Select`, `Textarea` | `components/ui/form/` | Formulářové prvky s jednou sadou tříd (`controlClass`). Vlastní `inputClass` ve stránce je chyba. |
|
||||
| `FilePicker` | `components/ui/form/FilePicker.tsx` | Vyber priloh: tlacitko, skryty `<input type="file">`, seznam vybranych, kontrola poctu a velikosti pri vyberu. Soubory drzi rodic. |
|
||||
| `readFileAsBase64`, `validateFiles`, `formatBytes`, `MAX_ATTACHMENT_BYTES` | `lib/files.ts` | Soubor na base64 bez prefixu `data:`, kontrola davky proti limitum, velikost pro cloveka. Limity stejne jako na serveru. |
|
||||
| `apiBlob(path)` | `lib/api.ts` | Stazeni binarniho obsahu s hlavickou `Authorization`. Obycejny odkaz token neposle. |
|
||||
| `useBrand()` | `hooks/useBrand.ts` | Udaje provozovatele z `/api/public/brand` nad statickou zalohou `config/brand.ts`. Jeden dotaz na nacteni stranky, sdileny. |
|
||||
| `Chip` | `components/ui/Chip.tsx` | Štítek. |
|
||||
| `Table`, `TableHead`, `Th`, `TableRow`, `Td` | `components/ui/Table.tsx` | Tabulka seznamu v portalu (hlavicka verzalkami, radky s linkou). Ctyri stranky ji kreslily kazda jinak. Huste tabulky ticketu a vykonu zustavaji zvlast, jsou to jine tabulky. |
|
||||
| `ServiceIcon` | `components/ui/ServiceIcon.tsx` | Ikona sluzby podle klice z katalogu. Misto `const Icon = serviceIcon(key)` v JSX, ktere lint hlasi jako komponentu vytvorenou pri vykresleni. |
|
||||
|
||||
@@ -57,6 +57,8 @@ byla, ale `viewer` mohl zalozit konektor nebo smazat automatizaci. Ted:
|
||||
| automatizace (zalozeni, uprava, smazani, novy token) | `automation.edit` |
|
||||
| stav incidentu | `incident.manage` za firmu incidentu; platformni jen spravce platformy |
|
||||
| vestavene akce na ticketu | pravo akce za firmu ticketu plus strop viditelnosti |
|
||||
| prilohy ticketu (nahrani, smazani) | `ticket.comment` za firmu ticketu plus strop viditelnosti; seznam a stazeni jen strop |
|
||||
| provozovatel portalu | jen spravce platformy, je to pole firmy |
|
||||
| prepnuti na jiny ucet, audit | `impersonate`, `audit.view` |
|
||||
|
||||
Spravce firmy s `user.manage` nenastavi `platformAdmin`, neprida clenstvi
|
||||
@@ -87,6 +89,23 @@ adresy se musi nahradit skutecnymi, jinak se ti lide neprihlasi. Firma pak
|
||||
nese `ico`, `dic`, `address` a `legalForm`, IC je unikatni. Endpointy jsou
|
||||
v [04-api.md](04-api.md).
|
||||
|
||||
### Provozovatel portalu a kontakt firmy
|
||||
|
||||
Zalozka Firmy (`components/dashboard/settings/TenantsAdmin.tsx`) ma
|
||||
u firmy prepinac **Provozovatel portalu** (`portalOperator`) a tri kontaktni
|
||||
pole: `contactEmail`, `contactPhone`, `website`. Provozovatel je firma,
|
||||
ktere chodi poptavky z verejneho kontaktniho formulare jako tickety
|
||||
a ze ktere web bere kontaktni a fakturacni udaje (`GET /api/public/brand`).
|
||||
V seznamu firem ma odznak "provozovatel".
|
||||
|
||||
Provozovatel je **prave jeden**. Zaskrtnuti u jine firmy sunda priznak te
|
||||
puvodni (`afterWrite` CRUD firem vola `clearOtherOperators`), obe zmeny
|
||||
prijdou do portalu jako `tenant.updated`. Nastavuje ho jen spravce platformy,
|
||||
stejne jako zaklada firmy - kdo portal provozuje, je nase rozhodnuti, ne
|
||||
zakaznicke. Kontaktni pole jde vyplnit u kazde firmy, vyznam maji jen
|
||||
u provozovatele; prazdne pole se uklada jako `null`. Duvody
|
||||
v [07-firmy-a-prava.md](07-firmy-a-prava.md).
|
||||
|
||||
## Navigace chodí ze serveru
|
||||
|
||||
Co uživatel vidí za záložky, je průnik dvou věcí:
|
||||
@@ -144,6 +163,12 @@ Widget je definice zdroje dat plus způsob zobrazení. Zdroj je ticket nebo
|
||||
automatizace, k tomu filtr a případné seskupení (podle stavu, kanálu, řešitele,
|
||||
typu, tagu).
|
||||
|
||||
Kresleni "Graf" ma dva tvary podle zdroje: casova rada je krivka, pocet
|
||||
ticketu se seskupenim je kolac (`widgets/PieChart.tsx`, nejvys osm vysecu,
|
||||
zbytek jako "ostatni"). Pocet bez seskupeni graf odmitne uz pri ulozeni,
|
||||
kolac z jedne vysece nic nerika. Tataz seskupena data jako "Tabulka" jsou
|
||||
sloupce: sloupce srovnavaji, kolac ukazuje podily.
|
||||
|
||||
Data pro celý přehled chodí **jedním requestem**. Deset dlaždic nesmí znamenat
|
||||
deset dotazů.
|
||||
|
||||
|
||||
@@ -56,6 +56,16 @@ by `3` a `"3"` byly dva tickety a nikdo by nepoznal proč.
|
||||
Bez `externalId` se vždycky zakládá nový ticket. Hádat podle předmětu by
|
||||
slučovalo věci, které spolu nesouvisí.
|
||||
|
||||
### Poptavka z webu je taky ticket
|
||||
|
||||
Kontaktni formular na verejnem webu nechodi pres webhook ani token:
|
||||
`POST /api/contact` zalozi ticket primo firme oznacene jako provozovatel
|
||||
portalu (`Tenant.portalOperator`). Kanal je `form`, `externalSource` je
|
||||
`web-form` a `externalId` je `null` - kazda poptavka je novy ticket, neni
|
||||
podle ceho navesovat. Telo ticketu je JSON formulare, aby detail ukazal
|
||||
tabulku poli. Prilohy z formulare se ulozi k ticketu. Podrobnosti
|
||||
v [06-tickety.md](06-tickety.md).
|
||||
|
||||
### Událost není řádek logu
|
||||
|
||||
Dvě různé věci, které se snadno pletou:
|
||||
@@ -70,6 +80,13 @@ přijatý JSON. Když se někdo ptá, proč ticket vypadá takhle, je to jediná
|
||||
odpověď. Drží se jich nejvýš 200 na ticket, starší se odmazávají - nekonečně
|
||||
rostoucí ticket by při každém zápisu přepisoval víc a víc dat.
|
||||
|
||||
Treti vec vedle udalosti a logu je **priloha**: soubor k ticketu. Neni
|
||||
soucasti ticketu ani udalosti, lezi ve vlastni kolekci `attachment`
|
||||
a ticket o ni vi jen radkem v logu (`Příloha: nazev (velikost)`). Detail
|
||||
ma vlastni sekci Prilohy se seznamem, stazenim, nahranim a smazanim; zapis
|
||||
chce `ticket.comment`. Model a limity jsou v [06-tickety.md](06-tickety.md),
|
||||
API v [04-api.md](04-api.md).
|
||||
|
||||
## Co se u ticketu měří
|
||||
|
||||
Aby šlo říct, kdo kolik odbavil a komu to nejde, nestačí počítat vyřešené.
|
||||
@@ -112,7 +129,7 @@ něco jiného.
|
||||
| Stránka | Co ukazuje |
|
||||
| -------------- | ----------------------------------------------------------- |
|
||||
| Tickety | seznam s filtry, **tabulka nebo dlaždice** |
|
||||
| Detail ticketu | obsah, události, log, akce, typ, tagy, řešitel, skupina |
|
||||
| Detail ticketu | obsah, události, log, přílohy, akce, typ, tagy, řešitel, skupina |
|
||||
| Lidé | řešitelé firmy a jejich vytížení, **tabulka nebo dlaždice** |
|
||||
| Detail osoby | její výkon, co má u sebe, co naposledy vyřešila |
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ pismo a znak, a co se pri prejmenovani menit nesmi.
|
||||
| ----------------------- | ------------------------------------ |
|
||||
| Barvy, pismo, utility | `web/src/index.css` |
|
||||
| Znak a slovni znacka | `web/src/components/layout/Logo.tsx` |
|
||||
| Nazev, kontakty, claim | `web/src/config/brand.ts` |
|
||||
| Kontakty a fakturacni udaje | provozovatel portalu v Nastaveni, Firmy (`GET /api/public/brand`) |
|
||||
| Nazev, claim, zaloha kontaktu | `web/src/config/brand.ts` |
|
||||
| Nazev na serveru | `BRAND_NAME`, vychozi v `src/config.ts` |
|
||||
| Titulek a fonty stranky | `web/index.html` |
|
||||
|
||||
@@ -34,6 +35,43 @@ Obe maji tutéž vychozi hodnotu, takze bez nastaveni cehokoliv sedi.
|
||||
zve, a ukazkova firma se jmenuje Automia. Neni to zbytek po prejmenovani, je
|
||||
to zaznam zakaznika - zadna promenna znacky ho nemeni.
|
||||
|
||||
### Kontakty bere web z provozovatele, `brand.ts` je jen zaloha
|
||||
|
||||
Kontaktni a fakturacni udaje uz nejsou natvrdo v kodu. Zdrojem je firma
|
||||
oznacena jako **provozovatel portalu** (`Tenant.portalOperator`, nastavuje
|
||||
spravce platformy v Nastaveni, Firmy - viz
|
||||
[07-firmy-a-prava.md](07-firmy-a-prava.md)). Server je vraci bez prihlaseni
|
||||
z `GET /api/public/brand` s `Cache-Control` na minutu, aby se zmena projevila
|
||||
bez restartu a bez noveho buildu klienta.
|
||||
|
||||
| Pole z provozovatele | Odkud na firme |
|
||||
| --------------------------- | ------------------------------------- |
|
||||
| `name`, `legalName` | `name` (obchodni jmeno, zvlastni pole neni) |
|
||||
| `ico`, `dic`, `address`, `legalForm` | udaje z ARES nebo rucne |
|
||||
| `email`, `phone`, `website` | `contactEmail`, `contactPhone`, `website` |
|
||||
|
||||
Na klientovi to sklada `useBrand()` (`web/src/hooks/useBrand.ts`): vraci
|
||||
hned statickou hodnotu z `brand.ts`, po odpovedi serveru ji **po polich**
|
||||
vymeni - `null` ze serveru nechava zalohu, takze castecne vyplneny
|
||||
provozovatel nerozbije paticku. Bez provozovatele nebo bez odpovedi zustane
|
||||
`brand.ts` cely. Nacita se jednou za nacteni stranky a sdili pres
|
||||
`useSyncExternalStore`, aby paticka, kontakt a vyzva k akci neposlaly tri
|
||||
stejne dotazy.
|
||||
|
||||
Pouziva ho `Contact`, `Footer`, `CallToAction`, `About`, `Login`, `Logo`
|
||||
a `usePageMeta`. Kdo pise novy kus webu s kontaktem, bere `useBrand()`,
|
||||
ne `brand` primo.
|
||||
|
||||
Co **zustava staticke** v `brand.ts` a provozovatel to nema: `claim`,
|
||||
`description`, `founded`, `social` a `support` (otevirajici doba, SLA).
|
||||
Jsou to texty znacky, ne udaje firmy. Adresa ze serveru je jedna textova
|
||||
radka, staticka zaloha ma tri (ulice, PSC a mesto, zeme); `website` zaloha
|
||||
nema, bez provozovatele je `null`.
|
||||
|
||||
Kontaktni formular na webu (`pages/Contact.tsx`) ma vedle poli i vyber
|
||||
priloh (`FilePicker`, nejvys 3 soubory po 5 MB) a poptavka konci jako ticket
|
||||
provozovatele, viz [06-tickety.md](06-tickety.md).
|
||||
|
||||
## Barvy
|
||||
|
||||
| Token | Hodnota | K cemu |
|
||||
@@ -142,7 +180,7 @@ zakaznika v ukazkovych datech a prejmenovat ho nema duvod.
|
||||
| Chybi | Poznamka |
|
||||
| --------------------- | --------------------------------------------------- |
|
||||
| Materialy mimo web | prezentace, obrazky pro socialni site |
|
||||
| Domena | `worknuke.cz` v `brand.ts`, ale nikde neni nasazena |
|
||||
| Domena | `worknuke.cz` v `brand.ts` jako zaloha, web provozovatele az z Nastaveni |
|
||||
| Prochazeni `accent-*` | 341 mist, ktera uz nemaji duvod pouzivat druhy klic |
|
||||
|
||||
## Rozmazani jen tam, kde neco prekryva
|
||||
|
||||
@@ -185,6 +185,7 @@ coz je cil, to sedi.
|
||||
| kompaktni karta ticketu | `components/dashboard/TicketCard.tsx`, varianta `compact` |
|
||||
| stitek | `components/ui/Chip.tsx` |
|
||||
| sklonovani poctu | `plural()` v `lib/format.ts` |
|
||||
| vyber priloh | `FilePicker` v `components/ui/form/FilePicker.tsx`, soubory v `lib/files.ts` |
|
||||
|
||||
Zmizelo 15 kopii trid vstupniho pole. Navrh zustava nize jako zduvodneni,
|
||||
proc to vypada tak, jak vypada.
|
||||
@@ -271,6 +272,16 @@ Sprava entit je rozdelena na `EntityAdmin` (tabulka, mazani) a `EntityForm`
|
||||
(formular v modalu); nastaveni ma `settings/FeaturesAdmin` a `settings/AuditView`
|
||||
jako vlastni komponenty, `Settings.tsx` je jen sklada.
|
||||
|
||||
Do vrstvy pribyl `FilePicker` (zari 2026, s prilohami ticketu): tlacitko,
|
||||
skryty `<input type="file">` a seznam vybranych souboru, ve dvou velikostech
|
||||
jako ostatni vstupy. Nativni vstup se nekresli, protoze se neda ostylovat
|
||||
jednotne a jeho popisek "Soubor nevybran" se neda prelozit. Soubory drzi rodic
|
||||
(`files` / `onChange`), komponenta jen pridava a odebira a pri vyberu
|
||||
kontroluje pocet a velikost (`validateFiles` v `lib/files.ts`, limity stejne
|
||||
jako na serveru). Pouziva ho kontaktni formular na webu (3 soubory) a sekce
|
||||
priloh v detailu ticketu (10 na ticket). Vlastni `<input type="file">` ve
|
||||
strance je od ted stejna chyba jako vlastni `inputClass`.
|
||||
|
||||
---
|
||||
|
||||
## 3 - Hledani jako modal s kriterii
|
||||
|
||||
@@ -2,6 +2,106 @@
|
||||
|
||||
Nejnovejsi nahore.
|
||||
|
||||
## 2026-09-09 - Kolacovy graf ve vlastnim widgetu
|
||||
|
||||
Vlastni widget s kreslenim "Graf" bral jen casovou radu; pokus o graf
|
||||
ticketu podle stavu skoncil hlaskou "Graf umi jen zdroj Casova rada". Server
|
||||
pritom seskupeny pocet uz umel (`ticketCount` s `groupBy`), jen ho klient
|
||||
kreslil vzdy jako sloupce.
|
||||
|
||||
`validateWidget` pousti graf i nad `ticketCount` se seskupenim (bez
|
||||
seskupeni dal ne, kolac z jedne vysece nic nerika) a klient seskupena data
|
||||
u kreslení "Graf" vykresli jako kolac (`widgets/PieChart.tsx`, cisté SVG bez
|
||||
knihovny, osm barev z tokenu, zbytek nad osm skupin jako "ostatni", legenda
|
||||
s cislem a podilem, odkaz na filtrovany seznam). Napoveda ve Widgetech ma
|
||||
priklad a uz nepouziva anglicke stavy `new/open/waiting`, ktere v datech
|
||||
nejsou (filtr `closed: false`). Test `tests/data/customWidgets.test.ts`.
|
||||
|
||||
## 2026-09-09 - Poptavka z webu je ticket, prilohy, udaje provozovatele
|
||||
|
||||
Kontaktni formular na webu poptavku jen zalogoval: kdo nesledoval log
|
||||
containeru, o ni nevedel, a odesilatel dostal "ozveme se", ktere nikdo
|
||||
nemohl splnit. Zaroven mel web kontakty a IC natvrdo v `brand.ts`, takze
|
||||
zmena telefonu znamenala novy build. Obe veci maji spolecny koren: portal
|
||||
nevedel, **ktera firma ho provozuje**. Tahle zmena to zavadi a stavi na tom.
|
||||
|
||||
### Provozovatel portalu
|
||||
|
||||
Firma dostala priznak `portalOperator` a kontaktni pole `contactEmail`,
|
||||
`contactPhone`, `website` (vedle `ico`, `dic`, `address`, `legalForm`).
|
||||
Nastavuje ho spravce platformy v Nastaveni, Firmy
|
||||
(`components/dashboard/settings/TenantsAdmin.tsx`, vyclenene ze `Settings.tsx`).
|
||||
|
||||
| Rozhodnuti | Proc |
|
||||
| ----------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| priznak na firme, ne promenna | provozovatel je zaznam s IC a adresou, ktery uz v datech je; promenna by ho jen duplikovala |
|
||||
| prave jeden | dve firmy s poptavkami by znamenaly, ze ticket vznikne jen jedne a nikdo nevi ktere |
|
||||
| nastaveni u jine firmy sunda puvodni | `afterWrite` CRUD firem vola `clearOtherOperators`; zmenene firmy jdou do streamu jako `tenant.updated` |
|
||||
| jen spravce platformy | kdo portal provozuje, je nase rozhodnuti, stejne jako zalozeni firmy |
|
||||
| vypnuta firma provozovatelem neni | `operatorTenant` bere jen zapnutou, i kdyz priznak zustal |
|
||||
|
||||
### Poptavka z webu je ticket
|
||||
|
||||
`POST /api/contact` zaklada ticket provozovateli: kanal `form`, predmet
|
||||
`Poptávka: <tema>`, tagy `Poptávka` a tema, zakaznik z formulare,
|
||||
`externalSource` `web-form`, poznamka v logu ticketu. **Telo ticketu je JSON
|
||||
formulare** (`name`, `email`, `company`, `phone`, `topic`, `message`,
|
||||
`receivedAt`), ne slozena veta: `TicketBody` JSON ukaze jako tabulku
|
||||
a zadne pole se neztrati v prose; rozhodnuti zadavatele. Bez provozovatele
|
||||
se poptavka jen zaloguje (warn) a odpoved je porad 202, zvenku nema byt
|
||||
poznat, jak je portal nastaveny. Limit 5 za hodinu z adresy se nemeni
|
||||
a bezi **pred** parserem tela, aby vycerpane pokusy nenutily server cist
|
||||
megabajty. Formular bere az 3 prilohy po 5 MB (`FilePicker` v `Contact.tsx`).
|
||||
|
||||
Upozorneni e-mailem na novou poptavku zatim neni, resitel ji vidi
|
||||
v seznamu ticketu.
|
||||
|
||||
### Prilohy ticketu
|
||||
|
||||
Nova kolekce `attachment` (`src/data/attachments.ts`, typ
|
||||
v `src/shared/attachments.ts`), **ne pole ticketu**: obsah je velky, meni
|
||||
se nezavisle na ticketu a seznam se vraci bez nej. Obsah se uklada jako
|
||||
base64 uvnitr zaznamu obecneho uloziste. Zadna nova zavislost (multer,
|
||||
S3 klient): prototyp uklada jednotky MB a `defineStore` to zvladne; cenou
|
||||
je, ze v rezimu `file` kazda zmena prepise cely `attachment.json`. Blob
|
||||
uloziste (S3 nebo `bytea`) je dalsi krok, rozhrani modulu se tim nezmeni.
|
||||
|
||||
| Co | Hodnota |
|
||||
| ------------------------- | ----------------------------------------------------------------------- |
|
||||
| limity | 5 MB na soubor po dekodovani, 10 na ticket, nazev 200 znaku |
|
||||
| kontrola | `checkUploads` nad celou davkou pred prvnim zapisem: bud vse, nebo nic |
|
||||
| nazev | `sanitizeName`: bez cesty a ridicich znaku, jinak rozbije `Content-Disposition` |
|
||||
| endpointy | `GET/POST /api/dashboard/tickets/:id/attachments`, `GET .../:attachmentId/content`, `DELETE .../:attachmentId` |
|
||||
| pravo | `ticket.comment` za firmu ticketu, ticket pres `visibleTicketOrDeny` jako u detailu |
|
||||
| stopa | radek v logu ticketu (`noteAttachment`), `ticket.updated`, audit `ticket.attachment.add` / `.remove` |
|
||||
| stazeni | binarni odpoved chce `Authorization`, portal stahuje pres `apiBlob` a docasny odkaz |
|
||||
|
||||
Globalni `express.json` ma 256 kB, coz na soubory nestaci. Kontakt a prilohy
|
||||
maji vlastni parser se stropem z `jsonLimitFor` (`src/routes/bodyLimit.ts`)
|
||||
a globalni je preskakuje (`hasOwnBodyLimit` v `app.ts`), jinak by telo
|
||||
odmitl driv, nez se k nemu router dostane.
|
||||
|
||||
Portal: sekce priloh v detailu ticketu (`TicketAttachments.tsx`), panel
|
||||
resitele vyclenen do `TicketAssignPanel.tsx`; `TicketDetail` tim sel pod
|
||||
500 radku. `FilePicker` v `ui/form`, prace se soubory v `lib/files.ts`.
|
||||
|
||||
### Verejny brand
|
||||
|
||||
`GET /api/public/brand` bez prihlaseni, `Cache-Control` na minutu, vraci
|
||||
`name`, `legalName`, `ico`, `dic`, `address`, `legalForm`, `email`, `phone`,
|
||||
`website` provozovatele, bez nej same `null`. Web to sklada v `hooks/useBrand.ts`
|
||||
nad statickou zalohou `config/brand.ts`: `null` ze serveru nechava zalohu,
|
||||
`claim`, `description`, `social`, `support` a `founded` zustavaji staticke.
|
||||
Pouziva to `Contact`, `Footer`, `CallToAction`, `About`, `Login`, `Logo`
|
||||
a `usePageMeta`. Jeden dotaz na nacteni stranky, sdileny pres
|
||||
`useSyncExternalStore`.
|
||||
|
||||
### Testy
|
||||
|
||||
12 souboru, 130 testu (22 novych): uloziste priloh (`tests/data/attachments.test.ts`),
|
||||
kontakt (`tests/routes/contact.test.ts`), routy priloh
|
||||
(`tests/routes/attachments.test.ts`), verejny brand (`tests/routes/public.test.ts`).
|
||||
|
||||
## 2026-09-09 - Struktura podle zasad: rozdeleni souboru, lint, testy
|
||||
|
||||
`D:\GitHubRepository\CLAUDE.md` dostal zasady pro vsechny projekty (struktura
|
||||
|
||||
Reference in New Issue
Block a user