diff --git a/documentation/07-firmy-a-prava.md b/documentation/07-firmy-a-prava.md index 4db56c7..fdaf44c 100644 --- a/documentation/07-firmy-a-prava.md +++ b/documentation/07-firmy-a-prava.md @@ -249,7 +249,13 @@ Tataz hierarchie, jen "moje" znamena neco jineho: zadavatel pozadavek nikdy nema prirazeny. Ticket proto nese `createdById` a plati, ze kdo vidi celou firmu, vidi vsechny jeji pozadavky, ostatni jen ty svoje. -### Co z toho plyne +### Nezarazene vidi kazdy -Neprirazeny ticket **bez skupiny** nevidi nikdo krome toho, kdo vidi celou -firmu. Prichozi praci musi nekdo smerovat, jinak se k radovym clenum nedostane. +Ticket **bez resitele** je ve stropu vzdycky, at uz ma skupinu nebo ne. + +Prvni verze ho schovavala a byla to diera v provozu: prichozi ticket, ktery +jeste nikdo nesmeroval, nepatri do zadne sekce, takze by ho nevidel nikdo +krome vedeni - a nikdo by si ho nevzal. **Fronta je spolecna, prave proto je +to fronta.** + +Jakmile si ho nekdo vezme, plati strop jako u kazdeho jineho ticketu. diff --git a/documentation/25-navrh-pristupny-portal.md b/documentation/25-navrh-pristupny-portal.md index 1e76a69..e62ad0f 100644 --- a/documentation/25-navrh-pristupny-portal.md +++ b/documentation/25-navrh-pristupny-portal.md @@ -1,10 +1,16 @@ # 25 - Navrh: pristupny portal a viditelnost -**Navrh, ne popis stavu.** Hotove uz jsou sekce 4 (viditelnost, vcetne zalozky -Firma) a 6, ostatni zatim naprogramovane neni. Popis toho, jak viditelnost -opravdu funguje, je v [07-firmy-a-prava.md](07-firmy-a-prava.md). 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). +**Navrh, ne popis stavu.** Hotove uz jsou: + +- sekce 4, viditelnost a zalozka Firma - jak to funguje, popisuje + [07-firmy-a-prava.md](07-firmy-a-prava.md), +- sekce 6, kontrakt webhooku, +- z prvni sekce dlazdice "Moje tickety" a "Fronta bez resitele" vcetne noveho + vychoziho rozlozeni. + +Zbyva widget akci, "Zaciname", formularova vrstva a hledani. 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 diff --git a/documentation/99-zmeny.md b/documentation/99-zmeny.md index c2f330f..82db638 100644 --- a/documentation/99-zmeny.md +++ b/documentation/99-zmeny.md @@ -2,6 +2,68 @@ Nejnovejsi nahore. +## 2026-09-02 - Nezarazene vidi kazdy, a dlazdice "Moje tickety" + +Dve veci, ktere ze stropu viditelnosti vypadly. + +### Nezarazeny ticket je ve stropu vzdycky + +Prvni verze ho schovavala: kdo nevidel na celou firmu, nevidel ticket, ktery +nema resitele ani skupinu. Byla to diera v provozu. Prichozi ticket, ktery +jeste nikdo nesmeroval, nepatri do zadne sekce, takze by ho nevidel nikdo krome +vedeni - a nikdo by si ho nevzal. **Fronta je spolecna, prave proto je to +fronta.** + +`withinVisibility` proto pousti kazdy ticket bez resitele, at uz ma skupinu nebo +ne. Jakmile si ho nekdo vezme, plati strop jako u kazdeho jineho. + +### Dlazdice "Moje tickety" a "Fronta bez resitele" + +Byly v navrhu od zacatku a nikdy se neudelaly. Presne jak navrh rikal, staci +zaznam v katalogu a zdroj v `builtinSources` - **zadna nova komponenta**, data +pocita tatáz cesta jako u vykonu resitelu. + +```ts +'list.myTickets': { kind: 'ticketList', filter: { assignee: ['me'], closed: false }, limit: 8 }, +'list.unassigned': { kind: 'ticketList', filter: { assignee: ['unassigned'], closed: false }, limit: 8 }, +``` + +K tomu tri veci, ktere si to vyzadalo: + +- **`closed` ve `WidgetTicketFilter`.** Bez nej se vyrizene tickety nedaly + odfiltrovat: stav je volny retezec a vyjmenovat vsechny podoby slova "hotovo" + se neda. Prospeje to i vlastnim widgetum, dosud neslo postavit "otevrene + tickety podle typu". +- **`ResolvedScope.personId` se plni vzdycky**, ne jen u pohledu `mine`. Prehled + se pta v pohledu `tenant`, takze filtr `me` nemel co dosadit a dlazdice + vracela prazdno. Kdo je to "ja", na pohledu nezalezi. +- **Kdo neni veden jako resitel, tomu se "Moje tickety" nenabidnou.** Ticket se + prirazuje resiteli, ne uctu, takze by takovy clovek koukal na prazdno navzdy + a nedozvedel se proc. + +### Vychozi rozlozeni + +Nahore to, co clovek muze udelat, teprve pod tim cisla: + +```text +Moje tickety, Fronta bez resitele +Aktivni automatizace, Otevrene tickety, Bezici incidenty +Posledni tickety, Incidenty +``` + +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 zustava. + +**Zmena se projevi jen tem, kdo si dashboard jeste neupravili.** Rozlozeni se +uklada za dvojici uzivatel a firma, kdo uz si ho osahal, musi dlazdice pridat +rucne nebo dat "Vychozi". + +### Overeno na bezici instanci + +Kriz jako radovy clen sekce: "Moje tickety" vraci jeho TK-4820, "Fronta bez +resitele" nezarazeny TK-4819, a v seznamu ticketu vidi oba. Pred opravou +`personId` vracela prvni dlazdice prazdno. + ## 2026-09-02 - Detail ticketu ma rozradovace, ne tri karty pod sebou Navrh v [25](25-navrh-pristupny-portal.md) mel detail jako jednu slozku diff --git a/src/data/access.ts b/src/data/access.ts index fa4a224..2d3ee9a 100644 --- a/src/data/access.ts +++ b/src/data/access.ts @@ -172,7 +172,16 @@ export interface ResolvedScope { tenantIds: string[]; /** Firma, na kterou je pohled zamereny. null u pohledu `all`. */ tenantId: string | null; - /** Vyplnene jen u pohledu `mine`. */ + /** + * Resitel prihlaseneho cloveka, nebo null. + * + * Vyplnene **vzdycky**, ne jen u pohledu `mine`. Driv se plnilo jen tam + * a widget "moje tickety" proto na prehledu vracel prazdno: prehled se + * pta v pohledu `tenant`, takze filtr `me` nemel co dosadit. + * + * Kdo je to "ja", na pohledu nezalezi. Jestli se podle toho ma filtrovat, + * rozhoduje volajici - `ticketFilterFrom` se pta na `scope.scope`. + */ personId: string | null; /** * Strop viditelnosti. **Pohled se o nej orizne**, nikdy naopak. @@ -228,7 +237,7 @@ export function resolveScope( scope, tenantIds: access.tenants.map((tenant) => tenant.id), tenantId: null, - personId: null, + personId: access.personId, visibility: { kind: 'all' }, }; } @@ -251,7 +260,7 @@ export function resolveScope( scope, tenantIds: [tenantId], tenantId, - personId: scope === 'mine' ? access.personId : null, + personId: access.personId, visibility: visibilityFor(user, tenantId), }; } diff --git a/src/data/customWidgets.ts b/src/data/customWidgets.ts index fadb6cf..dcec448 100644 --- a/src/data/customWidgets.ts +++ b/src/data/customWidgets.ts @@ -43,6 +43,13 @@ export interface WidgetTicketFilter { assignee?: string[]; groupIds?: string[]; period?: WidgetPeriod; + /** + * Jen vyrizene, nebo jen nevyrizene. Nevyplnene = oboji. + * + * Ptat se na to pres stav nejde: stav je volny retezec a vyjmenovat + * vsechny podoby slova "hotovo" se neda. Priznak `closed` je od toho. + */ + closed?: boolean; } export type WidgetSource = diff --git a/src/data/dashboardLayouts.ts b/src/data/dashboardLayouts.ts index 58454e0..6936314 100644 --- a/src/data/dashboardLayouts.ts +++ b/src/data/dashboardLayouts.ts @@ -24,14 +24,23 @@ export const MAX_WIDGETS = 12; /** Co uvidi nekdo, kdo si dashboard jeste neupravil. */ const defaultLayout: LayoutItem[] = [ - { id: 'w1', widgetId: 'stat.activeAutomations', size: 'third' }, - { id: 'w2', widgetId: 'stat.openTickets', size: 'third' }, - { id: 'w3', widgetId: 'stat.activeIncidents', size: 'third' }, - { id: 'w4', widgetId: 'chart.runs', size: 'full' }, - { id: 'w5', widgetId: 'list.tickets', size: 'half' }, - { id: 'w6', widgetId: 'list.incidents', size: 'half' }, + // Nahore to, co clovek muze udelat, teprve pod tim cisla. Vsechny ostatni + // dlazdice odpovidaji na "jak jsme na tom", tyhle dve na "co mam delat ted". + { id: 'w1', widgetId: 'list.myTickets', size: 'half' }, + { id: 'w2', widgetId: 'list.unassigned', size: 'half' }, + { id: 'w3', widgetId: 'stat.activeAutomations', size: 'third' }, + { id: 'w4', widgetId: 'stat.openTickets', size: 'third' }, + { id: 'w5', widgetId: 'stat.activeIncidents', size: 'third' }, + { id: 'w6', widgetId: 'list.tickets', size: 'half' }, + { id: 'w7', widgetId: 'list.incidents', size: 'half' }, ]; +/* + * Graf behu ve vychozi sade neni schvalne. Je to nejmene srozumitelna + * dlazdice pro noveho cloveka - behy ceho a co s tim - a zabira celou sirku. + * V katalogu zustava, kdo ho chce, prida si ho. + */ + const layouts = new Map(); /** diff --git a/src/data/ticketStore.ts b/src/data/ticketStore.ts index 1769136..53dc2b0 100644 --- a/src/data/ticketStore.ts +++ b/src/data/ticketStore.ts @@ -950,7 +950,14 @@ export interface TicketFilter { */ function withinVisibility(ticket: StoredTicket, visibility: Visibility): boolean { if (visibility.kind === 'all') return true; - if (ticket.assigneeId && visibility.personIds.includes(ticket.assigneeId)) return true; + /* + * **Nezarazene vidi kazdy ve firme.** Prvni verze stropu je schovavala + * a byla to diera v provozu: prichozi ticket, ktery jeste nikdo nesmeroval, + * nepatri do zadne sekce, takze by ho nevidel nikdo krome vedeni - a nikdo + * by si ho nevzal. Fronta je spolecna, prave proto je to fronta. + */ + if (ticket.assigneeId === null) return true; + if (visibility.personIds.includes(ticket.assigneeId)) return true; if (ticket.assigneeGroupId && visibility.groupIds.includes(ticket.assigneeGroupId)) return true; return false; } diff --git a/src/data/widgets.ts b/src/data/widgets.ts index 5f3f259..22c667c 100644 --- a/src/data/widgets.ts +++ b/src/data/widgets.ts @@ -132,6 +132,31 @@ export const widgets: WidgetDefinition[] = [ sizes: ['half', 'full'], defaultSize: 'full', }, + /* + * Dve dlazdice, ktere odpovidaji na "co mam delat ted". + * + * Vsechny ostatni odpovidaji na "jak jsme na tom" - jsou to statistiky + * a seznamy. Tyhle dve delaji z prehledu pracovni plochu misto reportu. + * + * Data pocita server pres `builtinSources` nize, stejne jako u vykonu + * resitelu, takze k nim neni potreba zadna nova komponenta. + */ + { + id: 'list.myTickets', + name: 'Moje tickety', + description: 'Co mám právě teď u sebe, nevyřízené nahoře.', + kind: 'list', + sizes: ['half', 'full'], + defaultSize: 'half', + }, + { + id: 'list.unassigned', + name: 'Fronta bez řešitele', + description: 'Co ještě nikdo nemá u sebe. Vidí ji každý, kdo do firmy patří.', + kind: 'list', + sizes: ['half', 'full'], + defaultSize: 'half', + }, ]; /** @@ -141,8 +166,23 @@ export const widgets: WidgetDefinition[] = [ * Diky tomu nemusi mit "Vykon resitelu" vlastni endpoint ani vlastni cestu * v klientovi - je to tentyz zdroj, jen ho nesklada uzivatel. */ -export const builtinSources: Record = { +export const builtinSources: Record = { 'panel.agents': { kind: 'agentStats', period: '30d' }, + /* + * `me` a `unassigned` umi filtr uz davno, jen je zatim nikdo nepouzil na + * vestaveny widget. `closed: false` je tam schvalne: vyrizene tickety na + * prehledu nikoho nezajimaji a bez toho by zabraly misto tem, co hori. + */ + 'list.myTickets': { + kind: 'ticketList', + filter: { assignee: ['me'], closed: false }, + limit: 8, + }, + 'list.unassigned': { + kind: 'ticketList', + filter: { assignee: ['unassigned'], closed: false }, + limit: 8, + }, }; export function findWidget(id: string): WidgetDefinition | undefined { @@ -166,7 +206,11 @@ export interface CatalogWidget extends WidgetDefinition { * znala jen pevne widgety a ulozit rozlozeni s vlastnim widgetem vzdycky * selhalo hlaskou, ze v katalogu neni. */ -export function widgetCatalog(tenantIds: string[], userId: string): CatalogWidget[] { +export function widgetCatalog( + tenantIds: string[], + userId: string, + hasPerson: boolean, +): CatalogWidget[] { const custom = listCustomWidgets(tenantIds, userId).map( (widget): CatalogWidget => ({ id: widget.id, @@ -183,5 +227,12 @@ export function widgetCatalog(tenantIds: string[], userId: string): CatalogWidge }), ); - return [...widgets.map((widget): CatalogWidget => ({ ...widget, custom: false })), ...custom]; + /* + * Kdo neni veden jako resitel, nema "moje tickety" co ukazat: ticket se + * prirazuje resiteli, ne uctu. Nabidnout mu prazdnou dlazdici by znamenalo, + * ze uvidi prazdno navzdy a nedozvi se proc. + */ + const builtin = widgets.filter((widget) => hasPerson || widget.id !== 'list.myTickets'); + + return [...builtin.map((widget): CatalogWidget => ({ ...widget, custom: false })), ...custom]; } diff --git a/src/routes/dashboard.ts b/src/routes/dashboard.ts index 6061634..7baf168 100644 --- a/src/routes/dashboard.ts +++ b/src/routes/dashboard.ts @@ -265,7 +265,9 @@ dashboardRouter.get('/widgets', (req, res) => { const tenantId = layoutTenant(req, res); if (!tenantId) return; - return res.json({ items: widgetCatalog([tenantId], req.user!.id) }); + return res.json({ + items: widgetCatalog([tenantId], req.user!.id, accessFor(req.user!, tenantId).personId !== null), + }); }); /** @@ -324,7 +326,7 @@ dashboardRouter.put('/layout', (req, res) => { const problems = validateLayout( parsed.data.items, - widgetCatalog([tenantId], req.user!.id), + widgetCatalog([tenantId], req.user!.id, accessFor(req.user!, tenantId).personId !== null), ); if (problems.length > 0) { console.warn(`[layout] ${req.user!.email}: neplatne rozlozeni - ${problems.join(' ')}`); diff --git a/src/routes/widgetData.ts b/src/routes/widgetData.ts index cb8ce7b..c01e145 100644 --- a/src/routes/widgetData.ts +++ b/src/routes/widgetData.ts @@ -138,6 +138,7 @@ function since(period: WidgetTicketFilter['period']): number | null { * da zapomenout. */ function matches(ticket: Ticket, filter: WidgetTicketFilter, personId: string | null): boolean { + if (filter.closed !== undefined && ticket.closed !== filter.closed) return false; if (filter.status && filter.status.length > 0 && !filter.status.includes(ticket.status)) return false; if (filter.channel && filter.channel.length > 0 && !filter.channel.includes(ticket.channel)) return false; if (filter.typeIds && filter.typeIds.length > 0) { diff --git a/web/src/pages/dashboard/Overview.tsx b/web/src/pages/dashboard/Overview.tsx index 7e1173e..df09a48 100644 --- a/web/src/pages/dashboard/Overview.tsx +++ b/web/src/pages/dashboard/Overview.tsx @@ -46,7 +46,7 @@ const span: Record = { * Vestavene widgety, jejichz data pocita server stejne jako u vlastnich. * Musi sedet s `builtinSources` v `src/data/widgets.ts`. */ -const COMPUTED_BUILTINS = new Set(['panel.agents']); +const COMPUTED_BUILTINS = new Set(['panel.agents', 'list.myTickets', 'list.unassigned']); const sizeLabels: Record = { third: 'třetina',