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:
JiriUhlir
2026-09-09 19:35:00 +02:00
parent 22dda2d139
commit c25e826766
72 changed files with 3978 additions and 1118 deletions
+100
View File
@@ -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