Stav ticketu je volny retezec, ciselnik pryc
Ciselnik new/open/waiting/resolved je pryc. Tickety chodi z cizich aplikaci, ktere maji svoje stavy - voicebot posila ringing a completed. Nutit je do nasi ctverice znamenalo, ze u ticketu svitilo "Novy", i kdyz byl podle odesilatele davno hotovy. Misto nej priznak `closed`: fronta, vytizeni i statistiky potrebuji vedet, co uz nikdo neresi, a z volneho retezce to poznat nejde. Nastavuje se sam podle `TicketType.closedStatuses`, a kdyz je typ nema, podle bezneho pojmenovani (vyreseno, hotovo, completed, closed). `stage` zruseno. Byla to obchazka, jak dostat cizi stavy do ticketu, aniz by se sahlo na ciselnik. Kdyz je stav volny, druhe pole na tutéz vec jen matlo. Vyber stavu v detailu nabizi stavy typu, doporucene a ten, ktery ticket ma prave ted, aby hodnota z cizi aplikace ze seznamu nezmizela. Filtr v seznamu nabizi stavy, ktere v datech opravdu jsou. Overeno 11 kontrolami: ticket z voicebota ma stav ringing, pak in-progress a completed, completed se pozna jako hotovo a zmizi z fronty, filtr i widget ukazuji tvoje stavy a rucne jde nastavit i "ceka na zpetne volani". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ef3080d3f6
commit
202d525905
+49
-16
@@ -48,10 +48,11 @@ import { listNotifications, markRead, unreadCount } from '../data/notifications.
|
||||
import { queueStats, recentRuns } from '../runtime/queue.js';
|
||||
import { recordAudit } from '../data/audit.js';
|
||||
import { findTenant, generateIntakeToken, refreshTenants, tenantStore } from '../data/tenants.js';
|
||||
import { listTicketTypes } from '../data/ticketTypes.js';
|
||||
import { findTicketType, listTicketTypes } from '../data/ticketTypes.js';
|
||||
import {
|
||||
addComment,
|
||||
assignTicket,
|
||||
findTicket,
|
||||
getAgentStats,
|
||||
getTicket,
|
||||
getWorkload,
|
||||
@@ -59,7 +60,6 @@ import {
|
||||
updateTicketStatus,
|
||||
type TicketChannel,
|
||||
type TicketFilter,
|
||||
type TicketStatus,
|
||||
} from '../data/ticketStore.js';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { validateRules } from '../scripts/mapping.js';
|
||||
@@ -264,10 +264,10 @@ dashboardRouter.get('/people/:id', (req, res) => {
|
||||
.filter((group) => group.personIds.includes(person.id))
|
||||
.map((group) => ({ id: group.id, name: group.name })),
|
||||
/** Co ma prave ted u sebe. */
|
||||
open: tickets.filter((ticket) => ticket.status !== 'resolved'),
|
||||
open: tickets.filter((ticket) => !ticket.closed),
|
||||
/** Co za posledni dobu vyresil, nejnovejsi nahore. */
|
||||
resolved: resolvedRecently
|
||||
.filter((ticket) => ticket.status === 'resolved')
|
||||
.filter((ticket) => ticket.closed)
|
||||
.sort((a, b) => (b.resolvedAt ?? '').localeCompare(a.resolvedAt ?? ''))
|
||||
.slice(0, 20),
|
||||
});
|
||||
@@ -398,7 +398,7 @@ function myOpenTickets(req: Request): number {
|
||||
return listTickets({
|
||||
tenantIds: access.tenants.map((tenant) => tenant.id),
|
||||
assignee: access.personId,
|
||||
}).filter((ticket) => ticket.status !== 'resolved').length;
|
||||
}).filter((ticket) => !ticket.closed).length;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- fronta
|
||||
@@ -452,7 +452,11 @@ dashboardRouter.get('/people', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
const ticketStatuses: TicketStatus[] = ['new', 'open', 'waiting', 'resolved'];
|
||||
/*
|
||||
* Stav uz neni ciselnik. Filtr proto bere, co prijde - kdyz to na nic nesedi,
|
||||
* vrati se prazdny seznam, coz je spravna odpoved na "ukaz mi stav, ktery
|
||||
* nikdo nema".
|
||||
*/
|
||||
const ticketChannels: TicketChannel[] = [
|
||||
'whatsapp',
|
||||
'facebook',
|
||||
@@ -475,8 +479,6 @@ function ticketFilterFrom(query: Record<string, unknown>, scope: ResolvedScope):
|
||||
if (typeof typeId === 'string' && typeId !== '') filter.typeId = typeId;
|
||||
const tag = query.tag;
|
||||
if (typeof tag === 'string' && tag !== '') filter.tag = tag;
|
||||
const stage = query.stage;
|
||||
if (typeof stage === 'string' && stage !== '') filter.stage = stage;
|
||||
const groupId = query.groupId;
|
||||
if (typeof groupId === 'string' && groupId !== '') filter.groupId = groupId;
|
||||
|
||||
@@ -493,12 +495,13 @@ function ticketFilterFrom(query: Record<string, unknown>, scope: ResolvedScope):
|
||||
}
|
||||
|
||||
function applyRest(query: Record<string, unknown>, filter: TicketFilter): TicketFilter {
|
||||
|
||||
/*
|
||||
* Stav uz neni ciselnik, takze se nekontroluje proti seznamu. Kdyz hodnota
|
||||
* na nic nesedi, vrati se prazdny seznam - to je spravna odpoved na dotaz
|
||||
* po stavu, ktery nikdo nema.
|
||||
*/
|
||||
const status = typeof query.status === 'string' ? query.status : undefined;
|
||||
if (status) {
|
||||
if (ticketStatuses.includes(status as TicketStatus)) filter.status = status as TicketStatus;
|
||||
else console.warn(`[tickets] neznamy stav ve filtru: ${status}`);
|
||||
}
|
||||
if (status) filter.status = status;
|
||||
|
||||
const channel = typeof query.channel === 'string' ? query.channel : undefined;
|
||||
if (channel) {
|
||||
@@ -516,8 +519,23 @@ dashboardRouter.get('/tickets', (req, res) => {
|
||||
const me = findPersonByEmail(req.user!.email);
|
||||
const filter = ticketFilterFrom(req.query as Record<string, unknown>, scope);
|
||||
|
||||
const items = listTickets(filter);
|
||||
|
||||
/*
|
||||
* Stavy, ktere firma opravdu pouziva. Stav je volny retezec, takze pevny
|
||||
* seznam v rozhrani by u ticketu z cizich aplikaci nikdy nesedel - filtr
|
||||
* musi nabidnout to, co v datech je.
|
||||
*
|
||||
* Bere se z celeho rozsahu, ne z vyfiltrovaneho vysledku: jinak by po
|
||||
* kliknuti na jeden stav zmizely ostatni a nesel by prepnout.
|
||||
*/
|
||||
const statuses = [
|
||||
...new Set(listTickets({ tenantIds: scope.tenantIds }).map((ticket) => ticket.status)),
|
||||
].sort((a, b) => a.localeCompare(b, 'cs'));
|
||||
|
||||
return res.json({
|
||||
items: listTickets(filter),
|
||||
items,
|
||||
statuses,
|
||||
meId: me?.id ?? null,
|
||||
scope: scope.scope,
|
||||
tenantId: scope.tenantId,
|
||||
@@ -584,7 +602,12 @@ dashboardRouter.post('/tickets/:id/assign', (req, res) => {
|
||||
});
|
||||
|
||||
const statusSchema = z.object({
|
||||
status: z.enum(['new', 'open', 'waiting', 'resolved']),
|
||||
/*
|
||||
* Stav je volny retezec, ne ciselnik. Tickety chodi z cizich aplikaci, ktere
|
||||
* maji svoje stavy, a nutit je do nasi ctverice znamenalo, ze uzivatel videl
|
||||
* nesmysl. Omezena je jen delka, aby se do pole nedal cely dokument.
|
||||
*/
|
||||
status: z.string().trim().min(1).max(60),
|
||||
});
|
||||
|
||||
dashboardRouter.post('/tickets/:id/status', (req, res) => {
|
||||
@@ -593,7 +616,17 @@ dashboardRouter.post('/tickets/:id/status', (req, res) => {
|
||||
return res.status(400).json({ error: 'validation_error', message: 'Neplatný stav ticketu.' });
|
||||
}
|
||||
|
||||
const ticket = updateTicketStatus(req.params.id, parsed.data.status, writableTenants(req));
|
||||
// Typ rika, ktere z jeho stavu znamenaji hotovo. Bez nej se to pozna
|
||||
// podle bezneho pojmenovani (vyreseno, completed).
|
||||
const current = findTicket(req.params.id, writableTenants(req));
|
||||
const type = current?.typeId ? findTicketType(current.typeId) : undefined;
|
||||
|
||||
const ticket = updateTicketStatus(
|
||||
req.params.id,
|
||||
parsed.data.status,
|
||||
writableTenants(req),
|
||||
type?.closedStatuses,
|
||||
);
|
||||
if (!ticket) {
|
||||
return res.status(404).json({ error: 'not_found', message: 'Ticket neexistuje.' });
|
||||
}
|
||||
|
||||
@@ -664,7 +664,7 @@ const sourceSchema = z.discriminatedUnion('kind', [
|
||||
kind: z.literal('ticketCount'),
|
||||
filter: z.record(z.unknown()).default({}),
|
||||
groupBy: z
|
||||
.enum(['assignee', 'group', 'status', 'stage', 'type', 'tag', 'channel'])
|
||||
.enum(['assignee', 'group', 'status', 'type', 'tag', 'channel'])
|
||||
.optional(),
|
||||
}),
|
||||
z.object({
|
||||
|
||||
@@ -179,8 +179,6 @@ function groupLabel(key: string, groupBy: WidgetGroupBy): string {
|
||||
return key === '' ? 'Bez typu' : (findTicketType(key)?.name ?? key);
|
||||
case 'status':
|
||||
return statusLabels[key] ?? key;
|
||||
case 'stage':
|
||||
return key === '' ? 'Bez fáze' : key;
|
||||
case 'channel':
|
||||
return channelLabels[key as keyof typeof channelLabels] ?? key;
|
||||
case 'tag':
|
||||
@@ -200,8 +198,6 @@ function groupKeys(ticket: Ticket, groupBy: string): string[] {
|
||||
return [ticket.typeId ?? ''];
|
||||
case 'status':
|
||||
return [ticket.status];
|
||||
case 'stage':
|
||||
return [ticket.stage ?? ''];
|
||||
case 'channel':
|
||||
return [ticket.channel];
|
||||
case 'tag':
|
||||
@@ -381,7 +377,6 @@ function ticketsHref(
|
||||
assignee: 'assignee',
|
||||
group: 'groupId',
|
||||
status: 'status',
|
||||
stage: 'stage',
|
||||
type: 'typeId',
|
||||
tag: 'tag',
|
||||
channel: 'channel',
|
||||
|
||||
Reference in New Issue
Block a user