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
+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) {
|
||||
|
||||
Reference in New Issue
Block a user