feat: export reports as xlsx and add machine inventory
This commit is contained in:
parent
29b865885c
commit
714b199879
34 changed files with 2304 additions and 245 deletions
|
|
@ -2,7 +2,8 @@
|
|||
import { mutation, query } from "./_generated/server";
|
||||
import type { MutationCtx, QueryCtx } from "./_generated/server";
|
||||
import { ConvexError, v } from "convex/values";
|
||||
import { Id, type Doc } from "./_generated/dataModel";
|
||||
import { Id, type Doc, type DataModel } from "./_generated/dataModel";
|
||||
import type { NamedTableInfo, Query as ConvexQuery } from "convex/server";
|
||||
|
||||
import { requireAdmin, requireStaff, requireUser } from "./rbac";
|
||||
|
||||
|
|
@ -176,7 +177,7 @@ async function normalizeTicketMentions(
|
|||
return output
|
||||
}
|
||||
|
||||
function normalizeStatus(status: string | null | undefined): TicketStatusNormalized {
|
||||
export function normalizeStatus(status: string | null | undefined): TicketStatusNormalized {
|
||||
if (!status) return "PENDING";
|
||||
const normalized = LEGACY_STATUS_MAP[status.toUpperCase()];
|
||||
return normalized ?? "PENDING";
|
||||
|
|
@ -651,6 +652,39 @@ function mapCustomFieldsToRecord(entries: NormalizedCustomField[] | undefined) {
|
|||
}, {});
|
||||
}
|
||||
|
||||
const DEFAULT_TICKETS_LIST_LIMIT = 250;
|
||||
const MIN_TICKETS_LIST_LIMIT = 25;
|
||||
const MAX_TICKETS_LIST_LIMIT = 600;
|
||||
const MAX_FETCH_LIMIT = 1000;
|
||||
const FETCH_MULTIPLIER_NO_SEARCH = 3;
|
||||
const FETCH_MULTIPLIER_WITH_SEARCH = 5;
|
||||
|
||||
type TicketsTableInfo = NamedTableInfo<DataModel, "tickets">;
|
||||
type TicketsQueryBuilder = ConvexQuery<TicketsTableInfo>;
|
||||
|
||||
function clampTicketLimit(limit: number) {
|
||||
if (!Number.isFinite(limit)) return DEFAULT_TICKETS_LIST_LIMIT;
|
||||
return Math.max(MIN_TICKETS_LIST_LIMIT, Math.min(MAX_TICKETS_LIST_LIMIT, Math.floor(limit)));
|
||||
}
|
||||
|
||||
function computeFetchLimit(limit: number, hasSearch: boolean) {
|
||||
const multiplier = hasSearch ? FETCH_MULTIPLIER_WITH_SEARCH : FETCH_MULTIPLIER_NO_SEARCH;
|
||||
const target = limit * multiplier;
|
||||
return Math.max(limit, Math.min(MAX_FETCH_LIMIT, target));
|
||||
}
|
||||
|
||||
function dedupeTicketsById(tickets: Doc<"tickets">[]) {
|
||||
const seen = new Set<string>();
|
||||
const result: Doc<"tickets">[] = [];
|
||||
for (const ticket of tickets) {
|
||||
const key = String(ticket._id);
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
result.push(ticket);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const list = query({
|
||||
args: {
|
||||
viewerId: v.optional(v.id("users")),
|
||||
|
|
@ -666,85 +700,138 @@ export const list = query({
|
|||
},
|
||||
handler: async (ctx, args) => {
|
||||
if (!args.viewerId) {
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
const viewerId = args.viewerId as Id<"users">;
|
||||
const { user, role } = await requireUser(ctx, viewerId, args.tenantId);
|
||||
if (role === "MANAGER" && !user.companyId) {
|
||||
throw new ConvexError("Gestor não possui empresa vinculada");
|
||||
}
|
||||
const { user, role } = await requireUser(ctx, args.viewerId, args.tenantId)
|
||||
|
||||
// Choose best index based on provided args for efficiency
|
||||
let base: Doc<"tickets">[] = [];
|
||||
if (role === "MANAGER") {
|
||||
if (!user.companyId) {
|
||||
throw new ConvexError("Gestor não possui empresa vinculada")
|
||||
const normalizedStatusFilter = args.status ? normalizeStatus(args.status) : null;
|
||||
const normalizedPriorityFilter = args.priority ? args.priority.toUpperCase() : null;
|
||||
const normalizedChannelFilter = args.channel ? args.channel.toUpperCase() : null;
|
||||
const searchTerm = args.search?.trim().toLowerCase() ?? null;
|
||||
|
||||
const requestedLimitRaw = typeof args.limit === "number" ? args.limit : DEFAULT_TICKETS_LIST_LIMIT;
|
||||
const requestedLimit = clampTicketLimit(requestedLimitRaw);
|
||||
const fetchLimit = computeFetchLimit(requestedLimit, Boolean(searchTerm));
|
||||
|
||||
const applyQueryFilters = (query: TicketsQueryBuilder) => {
|
||||
let working = query;
|
||||
if (normalizedStatusFilter) {
|
||||
working = working.filter((q) => q.eq(q.field("status"), normalizedStatusFilter));
|
||||
}
|
||||
// Managers are scoped to company; allow secondary narrowing by requester/assignee
|
||||
base = await ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_company", (q) => q.eq("tenantId", args.tenantId).eq("companyId", user.companyId!))
|
||||
.collect();
|
||||
if (normalizedPriorityFilter) {
|
||||
working = working.filter((q) => q.eq(q.field("priority"), normalizedPriorityFilter));
|
||||
}
|
||||
if (normalizedChannelFilter) {
|
||||
working = working.filter((q) => q.eq(q.field("channel"), normalizedChannelFilter));
|
||||
}
|
||||
if (args.queueId) {
|
||||
working = working.filter((q) => q.eq(q.field("queueId"), args.queueId!));
|
||||
}
|
||||
if (args.assigneeId) {
|
||||
working = working.filter((q) => q.eq(q.field("assigneeId"), args.assigneeId!));
|
||||
}
|
||||
if (args.requesterId) {
|
||||
working = working.filter((q) => q.eq(q.field("requesterId"), args.requesterId!));
|
||||
}
|
||||
return working;
|
||||
};
|
||||
|
||||
let base: Doc<"tickets">[] = [];
|
||||
|
||||
if (role === "MANAGER") {
|
||||
const baseQuery = applyQueryFilters(
|
||||
ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_company", (q) => q.eq("tenantId", args.tenantId).eq("companyId", user.companyId!))
|
||||
);
|
||||
base = await baseQuery.order("desc").take(fetchLimit);
|
||||
} else if (args.assigneeId) {
|
||||
base = await ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_assignee", (q) => q.eq("tenantId", args.tenantId).eq("assigneeId", args.assigneeId!))
|
||||
.collect();
|
||||
const baseQuery = applyQueryFilters(
|
||||
ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_assignee", (q) => q.eq("tenantId", args.tenantId).eq("assigneeId", args.assigneeId!))
|
||||
);
|
||||
base = await baseQuery.order("desc").take(fetchLimit);
|
||||
} else if (args.requesterId) {
|
||||
base = await ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_requester", (q) => q.eq("tenantId", args.tenantId).eq("requesterId", args.requesterId!))
|
||||
.collect();
|
||||
const baseQuery = applyQueryFilters(
|
||||
ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_requester", (q) => q.eq("tenantId", args.tenantId).eq("requesterId", args.requesterId!))
|
||||
);
|
||||
base = await baseQuery.order("desc").take(fetchLimit);
|
||||
} else if (args.queueId) {
|
||||
base = await ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_queue", (q) => q.eq("tenantId", args.tenantId).eq("queueId", args.queueId!))
|
||||
.collect();
|
||||
const baseQuery = applyQueryFilters(
|
||||
ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_queue", (q) => q.eq("tenantId", args.tenantId).eq("queueId", args.queueId!))
|
||||
);
|
||||
base = await baseQuery.order("desc").take(fetchLimit);
|
||||
} else if (normalizedStatusFilter) {
|
||||
const baseQuery = applyQueryFilters(
|
||||
ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_status", (q) => q.eq("tenantId", args.tenantId).eq("status", normalizedStatusFilter))
|
||||
);
|
||||
base = await baseQuery.order("desc").take(fetchLimit);
|
||||
} else if (role === "COLLABORATOR") {
|
||||
// Colaborador: exibir apenas tickets onde ele é o solicitante
|
||||
// Compatibilidade por e-mail: inclui tickets com requesterSnapshot.email == e-mail do viewer
|
||||
const all = await ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", args.tenantId))
|
||||
.collect()
|
||||
const viewerEmail = user.email.trim().toLowerCase()
|
||||
base = all.filter((t) => {
|
||||
if (t.requesterId === args.viewerId) return true
|
||||
const rs = t.requesterSnapshot as { email?: string } | undefined
|
||||
const email = typeof rs?.email === "string" ? rs.email.trim().toLowerCase() : null
|
||||
return Boolean(email && email === viewerEmail)
|
||||
})
|
||||
const viewerEmail = user.email.trim().toLowerCase();
|
||||
const directQuery = applyQueryFilters(
|
||||
ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_requester", (q) => q.eq("tenantId", args.tenantId).eq("requesterId", viewerId))
|
||||
);
|
||||
const directTickets = await directQuery.order("desc").take(fetchLimit);
|
||||
|
||||
let combined = directTickets;
|
||||
if (directTickets.length < fetchLimit) {
|
||||
const fallbackQuery = applyQueryFilters(
|
||||
ctx.db.query("tickets").withIndex("by_tenant", (q) => q.eq("tenantId", args.tenantId))
|
||||
);
|
||||
const fallbackRaw = await fallbackQuery.order("desc").take(fetchLimit);
|
||||
const fallbackMatches = fallbackRaw.filter((ticket) => {
|
||||
const snapshotEmail = (ticket.requesterSnapshot as { email?: string } | undefined)?.email;
|
||||
if (typeof snapshotEmail !== "string") return false;
|
||||
return snapshotEmail.trim().toLowerCase() === viewerEmail;
|
||||
});
|
||||
combined = dedupeTicketsById([...directTickets, ...fallbackMatches]);
|
||||
}
|
||||
base = combined.slice(0, fetchLimit);
|
||||
} else {
|
||||
base = await ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", args.tenantId))
|
||||
.collect();
|
||||
const baseQuery = applyQueryFilters(
|
||||
ctx.db.query("tickets").withIndex("by_tenant", (q) => q.eq("tenantId", args.tenantId))
|
||||
);
|
||||
base = await baseQuery.order("desc").take(fetchLimit);
|
||||
}
|
||||
|
||||
let filtered = base;
|
||||
|
||||
if (role === "MANAGER") {
|
||||
if (!user.companyId) {
|
||||
throw new ConvexError("Gestor não possui empresa vinculada")
|
||||
}
|
||||
filtered = filtered.filter((t) => t.companyId === user.companyId)
|
||||
filtered = filtered.filter((t) => t.companyId === user.companyId);
|
||||
}
|
||||
const normalizedStatusFilter = args.status ? normalizeStatus(args.status) : null;
|
||||
|
||||
if (args.priority) filtered = filtered.filter((t) => t.priority === args.priority);
|
||||
if (args.channel) filtered = filtered.filter((t) => t.channel === args.channel);
|
||||
if (normalizedPriorityFilter) filtered = filtered.filter((t) => t.priority === normalizedPriorityFilter);
|
||||
if (normalizedChannelFilter) filtered = filtered.filter((t) => t.channel === normalizedChannelFilter);
|
||||
if (args.assigneeId) filtered = filtered.filter((t) => String(t.assigneeId ?? "") === String(args.assigneeId));
|
||||
if (args.requesterId) filtered = filtered.filter((t) => String(t.requesterId) === String(args.requesterId));
|
||||
if (normalizedStatusFilter) {
|
||||
filtered = filtered.filter((t) => normalizeStatus(t.status) === normalizedStatusFilter);
|
||||
}
|
||||
if (args.search) {
|
||||
const term = args.search.toLowerCase();
|
||||
if (searchTerm) {
|
||||
filtered = filtered.filter(
|
||||
(t) =>
|
||||
t.subject.toLowerCase().includes(term) ||
|
||||
t.summary?.toLowerCase().includes(term) ||
|
||||
`#${t.reference}`.toLowerCase().includes(term)
|
||||
t.subject.toLowerCase().includes(searchTerm) ||
|
||||
t.summary?.toLowerCase().includes(searchTerm) ||
|
||||
`#${t.reference}`.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
const limited = args.limit ? filtered.slice(0, args.limit) : filtered;
|
||||
|
||||
const limited = filtered.slice(0, requestedLimit);
|
||||
const categoryCache = new Map<string, Doc<"ticketCategories"> | null>();
|
||||
const subcategoryCache = new Map<string, Doc<"ticketSubcategories"> | null>();
|
||||
const machineCache = new Map<string, Doc<"machines"> | null>();
|
||||
// hydrate requester and assignee
|
||||
const result = await Promise.all(
|
||||
limited.map(async (t) => {
|
||||
|
|
@ -774,6 +861,49 @@ export const list = query({
|
|||
subcategorySummary = { id: subcategory._id, name: subcategory.name };
|
||||
}
|
||||
}
|
||||
const machineSnapshot = t.machineSnapshot as
|
||||
| {
|
||||
hostname?: string
|
||||
persona?: string
|
||||
assignedUserName?: string
|
||||
assignedUserEmail?: string
|
||||
status?: string
|
||||
}
|
||||
| undefined;
|
||||
let machineSummary:
|
||||
| {
|
||||
id: Id<"machines"> | null
|
||||
hostname: string | null
|
||||
persona: string | null
|
||||
assignedUserName: string | null
|
||||
assignedUserEmail: string | null
|
||||
status: string | null
|
||||
}
|
||||
| null = null;
|
||||
if (t.machineId) {
|
||||
const cacheKey = String(t.machineId);
|
||||
if (!machineCache.has(cacheKey)) {
|
||||
machineCache.set(cacheKey, (await ctx.db.get(t.machineId)) as Doc<"machines"> | null);
|
||||
}
|
||||
const machineDoc = machineCache.get(cacheKey);
|
||||
machineSummary = {
|
||||
id: t.machineId,
|
||||
hostname: machineDoc?.hostname ?? machineSnapshot?.hostname ?? null,
|
||||
persona: machineDoc?.persona ?? machineSnapshot?.persona ?? null,
|
||||
assignedUserName: machineDoc?.assignedUserName ?? machineSnapshot?.assignedUserName ?? null,
|
||||
assignedUserEmail: machineDoc?.assignedUserEmail ?? machineSnapshot?.assignedUserEmail ?? null,
|
||||
status: machineDoc?.status ?? machineSnapshot?.status ?? null,
|
||||
};
|
||||
} else if (machineSnapshot) {
|
||||
machineSummary = {
|
||||
id: null,
|
||||
hostname: machineSnapshot.hostname ?? null,
|
||||
persona: machineSnapshot.persona ?? null,
|
||||
assignedUserName: machineSnapshot.assignedUserName ?? null,
|
||||
assignedUserEmail: machineSnapshot.assignedUserEmail ?? null,
|
||||
status: machineSnapshot.status ?? null,
|
||||
};
|
||||
}
|
||||
const serverNow = Date.now()
|
||||
return {
|
||||
id: t._id,
|
||||
|
|
@ -819,6 +949,7 @@ export const list = query({
|
|||
metrics: null,
|
||||
category: categorySummary,
|
||||
subcategory: subcategorySummary,
|
||||
machine: machineSummary,
|
||||
workSummary: {
|
||||
totalWorkedMs: t.totalWorkedMs ?? 0,
|
||||
internalWorkedMs: t.internalWorkedMs ?? 0,
|
||||
|
|
@ -867,6 +998,45 @@ export const getById = query({
|
|||
const assignee = t.assigneeId ? ((await ctx.db.get(t.assigneeId)) as Doc<"users"> | null) : null;
|
||||
const queue = t.queueId ? ((await ctx.db.get(t.queueId)) as Doc<"queues"> | null) : null;
|
||||
const company = t.companyId ? ((await ctx.db.get(t.companyId)) as Doc<"companies"> | null) : null;
|
||||
const machineSnapshot = t.machineSnapshot as
|
||||
| {
|
||||
hostname?: string
|
||||
persona?: string
|
||||
assignedUserName?: string
|
||||
assignedUserEmail?: string
|
||||
status?: string
|
||||
}
|
||||
| undefined;
|
||||
let machineSummary:
|
||||
| {
|
||||
id: Id<"machines"> | null
|
||||
hostname: string | null
|
||||
persona: string | null
|
||||
assignedUserName: string | null
|
||||
assignedUserEmail: string | null
|
||||
status: string | null
|
||||
}
|
||||
| null = null;
|
||||
if (t.machineId) {
|
||||
const machineDoc = (await ctx.db.get(t.machineId)) as Doc<"machines"> | null;
|
||||
machineSummary = {
|
||||
id: t.machineId,
|
||||
hostname: machineDoc?.hostname ?? machineSnapshot?.hostname ?? null,
|
||||
persona: machineDoc?.persona ?? machineSnapshot?.persona ?? null,
|
||||
assignedUserName: machineDoc?.assignedUserName ?? machineSnapshot?.assignedUserName ?? null,
|
||||
assignedUserEmail: machineDoc?.assignedUserEmail ?? machineSnapshot?.assignedUserEmail ?? null,
|
||||
status: machineDoc?.status ?? machineSnapshot?.status ?? null,
|
||||
};
|
||||
} else if (machineSnapshot) {
|
||||
machineSummary = {
|
||||
id: null,
|
||||
hostname: machineSnapshot.hostname ?? null,
|
||||
persona: machineSnapshot.persona ?? null,
|
||||
assignedUserName: machineSnapshot.assignedUserName ?? null,
|
||||
assignedUserEmail: machineSnapshot.assignedUserEmail ?? null,
|
||||
status: machineSnapshot.status ?? null,
|
||||
};
|
||||
}
|
||||
const queueName = normalizeQueueName(queue);
|
||||
const category = t.categoryId ? await ctx.db.get(t.categoryId) : null;
|
||||
const subcategory = t.subcategoryId ? await ctx.db.get(t.subcategoryId) : null;
|
||||
|
|
@ -1011,6 +1181,7 @@ export const getById = query({
|
|||
tags: t.tags ?? [],
|
||||
lastTimelineEntry: null,
|
||||
metrics: null,
|
||||
machine: machineSummary,
|
||||
category: category
|
||||
? {
|
||||
id: category._id,
|
||||
|
|
@ -1082,6 +1253,7 @@ export const create = mutation({
|
|||
assigneeId: v.optional(v.id("users")),
|
||||
categoryId: v.id("ticketCategories"),
|
||||
subcategoryId: v.id("ticketSubcategories"),
|
||||
machineId: v.optional(v.id("machines")),
|
||||
customFields: v.optional(
|
||||
v.array(
|
||||
v.object({
|
||||
|
|
@ -1147,6 +1319,15 @@ export const create = mutation({
|
|||
}
|
||||
}
|
||||
|
||||
let machineDoc: Doc<"machines"> | null = null
|
||||
if (args.machineId) {
|
||||
const machine = (await ctx.db.get(args.machineId)) as Doc<"machines"> | null
|
||||
if (!machine || machine.tenantId !== args.tenantId) {
|
||||
throw new ConvexError("Máquina inválida para este chamado")
|
||||
}
|
||||
machineDoc = machine
|
||||
}
|
||||
|
||||
const normalizedCustomFields = await normalizeCustomFieldValues(ctx, args.tenantId, args.customFields ?? undefined);
|
||||
// compute next reference (simple monotonic counter per tenant)
|
||||
const existing = await ctx.db
|
||||
|
|
@ -1156,14 +1337,20 @@ export const create = mutation({
|
|||
.take(1);
|
||||
const nextRef = existing[0]?.reference ? existing[0].reference + 1 : 41000;
|
||||
const now = Date.now();
|
||||
const initialStatus: TicketStatusNormalized = initialAssigneeId ? "AWAITING_ATTENDANCE" : "PENDING";
|
||||
const initialStatus: TicketStatusNormalized = "PENDING";
|
||||
const requesterSnapshot = {
|
||||
name: requester.name,
|
||||
email: requester.email,
|
||||
avatarUrl: requester.avatarUrl ?? undefined,
|
||||
teams: requester.teams ?? undefined,
|
||||
}
|
||||
const companyDoc = requester.companyId ? (await ctx.db.get(requester.companyId)) : null
|
||||
let companyDoc = requester.companyId ? (await ctx.db.get(requester.companyId)) : null
|
||||
if (!companyDoc && machineDoc?.companyId) {
|
||||
const candidateCompany = await ctx.db.get(machineDoc.companyId)
|
||||
if (candidateCompany && candidateCompany.tenantId === args.tenantId) {
|
||||
companyDoc = candidateCompany as Doc<"companies">
|
||||
}
|
||||
}
|
||||
const companySnapshot = companyDoc
|
||||
? { name: companyDoc.name, slug: companyDoc.slug, isAvulso: companyDoc.isAvulso ?? undefined }
|
||||
: undefined
|
||||
|
|
@ -1205,8 +1392,18 @@ export const create = mutation({
|
|||
requesterSnapshot,
|
||||
assigneeId: initialAssigneeId,
|
||||
assigneeSnapshot,
|
||||
companyId: requester.companyId ?? undefined,
|
||||
companyId: companyDoc?._id ?? requester.companyId ?? undefined,
|
||||
companySnapshot,
|
||||
machineId: machineDoc?._id ?? undefined,
|
||||
machineSnapshot: machineDoc
|
||||
? {
|
||||
hostname: machineDoc.hostname ?? undefined,
|
||||
persona: machineDoc.persona ?? undefined,
|
||||
assignedUserName: machineDoc.assignedUserName ?? undefined,
|
||||
assignedUserEmail: machineDoc.assignedUserEmail ?? undefined,
|
||||
status: machineDoc.status ?? undefined,
|
||||
}
|
||||
: undefined,
|
||||
working: false,
|
||||
activeSessionId: undefined,
|
||||
totalWorkedMs: 0,
|
||||
|
|
@ -1492,6 +1689,9 @@ export const updateStatus = mutation({
|
|||
const ticketDoc = ticket as Doc<"tickets">
|
||||
await requireTicketStaff(ctx, actorId, ticketDoc)
|
||||
const normalizedStatus = normalizeStatus(status)
|
||||
if (normalizedStatus === "AWAITING_ATTENDANCE" && !ticketDoc.activeSessionId) {
|
||||
throw new ConvexError("Inicie o atendimento antes de marcar o ticket como em andamento.")
|
||||
}
|
||||
const now = Date.now();
|
||||
await ctx.db.patch(ticketId, { status: normalizedStatus, updatedAt: now });
|
||||
await ctx.db.insert("ticketEvents", {
|
||||
|
|
@ -2006,6 +2206,26 @@ export const pauseWork = mutation({
|
|||
}
|
||||
|
||||
if (!ticketDoc.activeSessionId) {
|
||||
const normalizedStatus = normalizeStatus(ticketDoc.status)
|
||||
if (normalizedStatus === "AWAITING_ATTENDANCE") {
|
||||
const now = Date.now()
|
||||
await ctx.db.patch(ticketId, {
|
||||
status: "PAUSED",
|
||||
working: false,
|
||||
updatedAt: now,
|
||||
})
|
||||
await ctx.db.insert("ticketEvents", {
|
||||
ticketId,
|
||||
type: "STATUS_CHANGED",
|
||||
payload: {
|
||||
to: "PAUSED",
|
||||
toLabel: STATUS_LABELS.PAUSED,
|
||||
actorId,
|
||||
},
|
||||
createdAt: now,
|
||||
})
|
||||
return { status: "paused", durationMs: 0, pauseReason: reason, pauseNote: note ?? "", serverNow: now }
|
||||
}
|
||||
return { status: "already_paused" }
|
||||
}
|
||||
|
||||
|
|
@ -2278,15 +2498,21 @@ export const playNext = mutation({
|
|||
const chosen = candidates[0];
|
||||
const now = Date.now();
|
||||
const currentStatus = normalizeStatus(chosen.status);
|
||||
const nextStatus: TicketStatusNormalized =
|
||||
currentStatus === "PENDING" ? "AWAITING_ATTENDANCE" : currentStatus;
|
||||
const nextStatus: TicketStatusNormalized = currentStatus;
|
||||
const assigneeSnapshot = {
|
||||
name: agent.name,
|
||||
email: agent.email,
|
||||
avatarUrl: agent.avatarUrl ?? undefined,
|
||||
teams: agent.teams ?? undefined,
|
||||
}
|
||||
await ctx.db.patch(chosen._id, { assigneeId: agentId, assigneeSnapshot, status: nextStatus, updatedAt: now });
|
||||
await ctx.db.patch(chosen._id, {
|
||||
assigneeId: agentId,
|
||||
assigneeSnapshot,
|
||||
status: nextStatus,
|
||||
working: false,
|
||||
activeSessionId: undefined,
|
||||
updatedAt: now,
|
||||
});
|
||||
await ctx.db.insert("ticketEvents", {
|
||||
ticketId: chosen._id,
|
||||
type: "ASSIGNEE_CHANGED",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue