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 bez resitele a bez skupiny. 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. Dlazdice "Moje tickety" a "Fronta bez resitele" byly v navrhu od zacatku a nikdy se neudelaly. Presne jak navrh rikal: zaznam v katalogu a zdroj v builtinSources, zadna nova komponenta, data pocita tataz cesta jako u vykonu resitelu. 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 Vychozi rozlozeni ma nahore to, co clovek muze udelat, teprve pod tim cisla. Graf behu z vychozi sady ven, je to nejmene srozumitelna dlazdice pro noveho cloveka a zabira celou sirku. V katalogu zustava. Zmena se projevi jen tem, kdo si dashboard jeste neupravili. Overeno na bezici instanci: Kriz jako radovy clen sekce vidi ve svych ticketech TK-4820, ve fronte nezarazeny TK-4819 a v seznamu oba. Pred opravou personId vracela prvni dlazdice prazdno. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2a310137a2
commit
52999a961b
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+12
-3
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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<string, LayoutItem[]>();
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+54
-3
@@ -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<string, { kind: 'agentStats'; period: '30d' }> = {
|
||||
export const builtinSources: Record<string, WidgetSource> = {
|
||||
'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];
|
||||
}
|
||||
|
||||
@@ -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(' ')}`);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -46,7 +46,7 @@ const span: Record<WidgetSize, string> = {
|
||||
* 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<WidgetSize, string> = {
|
||||
third: 'třetina',
|
||||
|
||||
Reference in New Issue
Block a user