Dashboard: show ticket assignee on cards, widen layout to use the screen better

TicketDTO now carries assigneeName alongside the existing assigneeId — the
board's TicketCard shows it as a small avatar+name line ("Не назначено"
when nobody's assigned), so it's actually visible which admin/agent owns a
ticket without opening it. listTickets/listTicketsForCustomer resolve it
via a LEFT JOIN on users (avoids N+1 across a whole board); the handful of
single-ticket call sites (setTicketStatus, assignTicket, setTicketTags,
appendMessage, getTicketWithMessages) use a small getAssigneeName() lookup.

Also widened the admin layout's max-width from 7xl (1280px) to 1800px —
the 4-column kanban board was leaving a lot of unused space on wide
screens, cards were narrower than they needed to be. Verified visually via
Playwright screenshots at both 1920px and 1440px.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GteWhnWKTmnXcsd5jx6H7u
This commit is contained in:
ogrechkoandClaude Sonnet 5 committed 2026-08-21 06:01:52 +00:00
1 parent 1a31f72dd8
commit d9eaed41e0
4 files changed
+43 -10

No files matched your search

+29 -8
View File
@@ -32,7 +32,12 @@ type AttachmentInput = {
isInline?: boolean;
};
function toTicketDTO(ticket: typeof tickets.$inferSelect, customerName: string, tags: TagDTO[]): TicketDTO {
function toTicketDTO(
ticket: typeof tickets.$inferSelect,
customerName: string,
tags: TagDTO[],
assigneeName: string | null = null,
): TicketDTO {
return {
id: ticket.id,
subject: ticket.subject,
@@ -42,6 +47,7 @@ function toTicketDTO(ticket: typeof tickets.$inferSelect, customerName: string,
customerId: ticket.customerId,
customerName,
assigneeId: ticket.assigneeId,
assigneeName,
tags,
createdAt: ticket.createdAt.getTime(),
updatedAt: ticket.updatedAt.getTime(),
@@ -49,6 +55,13 @@ function toTicketDTO(ticket: typeof tickets.$inferSelect, customerName: string,
};
}
/** Single-ticket lookups only — listTickets/listTicketsForCustomer LEFT JOIN users directly instead, to avoid N+1 queries across a whole board. */
async function getAssigneeName(assigneeId: string | null): Promise<string | null> {
if (!assigneeId) return null;
const user = await db.query.users.findFirst({ where: eq(users.id, assigneeId) });
return user?.name ?? null;
}
function toMessageDTO(message: typeof messages.$inferSelect, attachments: AttachmentDTO[] = []): MessageDTO {
return {
id: message.id,
@@ -170,7 +183,7 @@ async function appendMessage(params: {
const ticket = await touchTicket(params.ticket.id, { lastMessageAt: new Date() });
const name = await customerName(ticket.customerId);
const ticketTagsList = await getTagsForTicket(ticket.id);
const dto = toTicketDTO(ticket, name, ticketTagsList);
const dto = toTicketDTO(ticket, name, ticketTagsList, await getAssigneeName(ticket.assigneeId));
const messageDto = toMessageDTO(message, attachmentDTOs);
publishTicketEvent({
@@ -437,14 +450,14 @@ export async function setTicketStatus(
status: (typeof tickets.$inferSelect)["status"],
) {
const ticket = await touchTicket(ticketId, { status });
const dto = toTicketDTO(ticket, await customerName(ticket.customerId), await getTagsForTicket(ticket.id));
const dto = toTicketDTO(ticket, await customerName(ticket.customerId), await getTagsForTicket(ticket.id), await getAssigneeName(ticket.assigneeId));
publishTicketEvent({ type: "ticket.updated", ticket: dto });
return dto;
}
export async function assignTicket(ticketId: string, assigneeId: string | null) {
const ticket = await touchTicket(ticketId, { assigneeId });
const dto = toTicketDTO(ticket, await customerName(ticket.customerId), await getTagsForTicket(ticket.id));
const dto = toTicketDTO(ticket, await customerName(ticket.customerId), await getTagsForTicket(ticket.id), await getAssigneeName(ticket.assigneeId));
publishTicketEvent({ type: "ticket.updated", ticket: dto });
return dto;
}
@@ -485,9 +498,10 @@ export async function listTickets(
}
const rows = await db
.select({ ticket: tickets, customerName: customers.displayName })
.select({ ticket: tickets, customerName: customers.displayName, assigneeName: users.name })
.from(tickets)
.innerJoin(customers, eq(tickets.customerId, customers.id))
.leftJoin(users, eq(tickets.assigneeId, users.id))
.orderBy(desc(tickets.lastMessageAt));
const idsToKeep = matchingTicketIds;
@@ -498,7 +512,9 @@ export async function listTickets(
}
const tagsMap = await getTagsForTickets(filteredRows.map((r) => r.ticket.id));
return filteredRows.map((row) => toTicketDTO(row.ticket, row.customerName, tagsMap.get(row.ticket.id) ?? []));
return filteredRows.map((row) =>
toTicketDTO(row.ticket, row.customerName, tagsMap.get(row.ticket.id) ?? [], row.assigneeName ?? null),
);
}
export async function getTicketWithMessages(ticketId: string) {
@@ -519,7 +535,7 @@ export async function getTicketWithMessages(ticketId: string) {
const ticketTagsList = await getTagsForTicket(ticketId);
return {
ticket: toTicketDTO(row[0].ticket, row[0].customerName, ticketTagsList),
ticket: toTicketDTO(row[0].ticket, row[0].customerName, ticketTagsList, await getAssigneeName(row[0].ticket.assigneeId)),
messages: ticketMessages.map((m) => toMessageDTO(m, attachmentsMap.get(m.id) ?? [])),
};
}
@@ -729,7 +745,12 @@ export async function setTicketTags(ticketId: string, tagIds: string[]): Promise
const ticket = await db.query.tickets.findFirst({ where: eq(tickets.id, ticketId) });
if (ticket) {
const dto = toTicketDTO(ticket, await customerName(ticket.customerId), await getTagsForTicket(ticketId));
const dto = toTicketDTO(
ticket,
await customerName(ticket.customerId),
await getTagsForTicket(ticketId),
await getAssigneeName(ticket.assigneeId),
);
publishTicketEvent({ type: "ticket.updated", ticket: dto });
}
return getTagsForTicket(ticketId);