feat: add ticket category model and align ticket ui\n\nCo-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

This commit is contained in:
esdrasrenan 2025-10-05 00:00:14 -03:00
parent 55511f3a0e
commit fab1cbe476
17 changed files with 1121 additions and 42 deletions

View file

@ -47,6 +47,8 @@ export const list = query({
);
}
const limited = args.limit ? filtered.slice(0, args.limit) : filtered;
const categoryCache = new Map<string, Doc<"ticketCategories"> | null>();
const subcategoryCache = new Map<string, Doc<"ticketSubcategories"> | null>();
// hydrate requester and assignee
const result = await Promise.all(
limited.map(async (t) => {
@ -54,6 +56,26 @@ export const list = 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 activeSession = t.activeSessionId ? await ctx.db.get(t.activeSessionId) : null;
let categorySummary: { id: Id<"ticketCategories">; name: string } | null = null;
let subcategorySummary: { id: Id<"ticketSubcategories">; name: string } | null = null;
if (t.categoryId) {
if (!categoryCache.has(t.categoryId)) {
categoryCache.set(t.categoryId, await ctx.db.get(t.categoryId));
}
const category = categoryCache.get(t.categoryId);
if (category) {
categorySummary = { id: category._id, name: category.name };
}
}
if (t.subcategoryId) {
if (!subcategoryCache.has(t.subcategoryId)) {
subcategoryCache.set(t.subcategoryId, await ctx.db.get(t.subcategoryId));
}
const subcategory = subcategoryCache.get(t.subcategoryId);
if (subcategory) {
subcategorySummary = { id: subcategory._id, name: subcategory.name };
}
}
return {
id: t._id,
reference: t.reference,
@ -89,6 +111,8 @@ export const list = query({
tags: t.tags ?? [],
lastTimelineEntry: null,
metrics: null,
category: categorySummary,
subcategory: subcategorySummary,
workSummary: {
totalWorkedMs: t.totalWorkedMs ?? 0,
activeSession: activeSession
@ -115,6 +139,8 @@ export const getById = query({
const requester = (await ctx.db.get(t.requesterId)) as Doc<"users"> | null;
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 category = t.categoryId ? await ctx.db.get(t.categoryId) : null;
const subcategory = t.subcategoryId ? await ctx.db.get(t.subcategoryId) : null;
const comments = await ctx.db
.query("ticketComments")
.withIndex("by_ticket", (q) => q.eq("ticketId", id))
@ -191,6 +217,19 @@ export const getById = query({
tags: t.tags ?? [],
lastTimelineEntry: null,
metrics: null,
category: category
? {
id: category._id,
name: category.name,
}
: null,
subcategory: subcategory
? {
id: subcategory._id,
name: subcategory.name,
categoryId: subcategory.categoryId,
}
: null,
workSummary: {
totalWorkedMs: t.totalWorkedMs ?? 0,
activeSession: activeSession
@ -223,12 +262,22 @@ export const create = mutation({
channel: v.string(),
queueId: v.optional(v.id("queues")),
requesterId: v.id("users"),
categoryId: v.id("ticketCategories"),
subcategoryId: v.id("ticketSubcategories"),
},
handler: async (ctx, args) => {
const subject = args.subject.trim();
if (subject.length < 3) {
throw new ConvexError("Informe um assunto com pelo menos 3 caracteres");
}
const category = await ctx.db.get(args.categoryId);
if (!category || category.tenantId !== args.tenantId) {
throw new ConvexError("Categoria inválida");
}
const subcategory = await ctx.db.get(args.subcategoryId);
if (!subcategory || subcategory.categoryId !== args.categoryId || subcategory.tenantId !== args.tenantId) {
throw new ConvexError("Subcategoria inválida");
}
// compute next reference (simple monotonic counter per tenant)
const existing = await ctx.db
.query("tickets")
@ -246,6 +295,8 @@ export const create = mutation({
priority: args.priority,
channel: args.channel,
queueId: args.queueId,
categoryId: args.categoryId,
subcategoryId: args.subcategoryId,
requesterId: args.requesterId,
assigneeId: undefined,
working: false,
@ -409,6 +460,58 @@ export const changeQueue = mutation({
},
});
export const updateCategories = mutation({
args: {
ticketId: v.id("tickets"),
categoryId: v.id("ticketCategories"),
subcategoryId: v.id("ticketSubcategories"),
actorId: v.id("users"),
},
handler: async (ctx, { ticketId, categoryId, subcategoryId, actorId }) => {
const ticket = await ctx.db.get(ticketId)
if (!ticket) {
throw new ConvexError("Ticket não encontrado")
}
const category = await ctx.db.get(categoryId)
if (!category || category.tenantId !== ticket.tenantId) {
throw new ConvexError("Categoria inválida")
}
const subcategory = await ctx.db.get(subcategoryId)
if (!subcategory || subcategory.categoryId !== categoryId || subcategory.tenantId !== ticket.tenantId) {
throw new ConvexError("Subcategoria inválida")
}
if (ticket.categoryId === categoryId && ticket.subcategoryId === subcategoryId) {
return { status: "unchanged" }
}
const now = Date.now()
await ctx.db.patch(ticketId, {
categoryId,
subcategoryId,
updatedAt: now,
})
const actor = (await ctx.db.get(actorId)) as Doc<"users"> | null
await ctx.db.insert("ticketEvents", {
ticketId,
type: "CATEGORY_CHANGED",
payload: {
categoryId,
categoryName: category.name,
subcategoryId,
subcategoryName: subcategory.name,
actorId,
actorName: actor?.name,
actorAvatar: actor?.avatarUrl,
},
createdAt: now,
})
return { status: "updated" }
},
})
export const workSummary = query({
args: { ticketId: v.id("tickets") },
handler: async (ctx, { ticketId }) => {