chore: prep platform improvements

This commit is contained in:
Esdras Renan 2025-11-09 21:09:38 -03:00
parent a62f3d5283
commit c5ddd54a3e
24 changed files with 777 additions and 649 deletions

View file

@ -3,8 +3,7 @@ import { mutation, query } from "./_generated/server";
import { api } from "./_generated/api";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import { ConvexError, v } from "convex/values";
import { Id, type Doc, type DataModel } from "./_generated/dataModel";
import type { NamedTableInfo, Query as ConvexQuery } from "convex/server";
import { Id, type Doc } from "./_generated/dataModel";
import { requireAdmin, requireStaff, requireUser } from "./rbac";
import {
@ -477,13 +476,15 @@ async function fetchTemplateSummaries(ctx: AnyCtx, tenantId: string): Promise<Te
async function fetchTicketFieldsByScopes(
ctx: QueryCtx,
tenantId: string,
scopes: string[]
scopes: string[],
companyId: Id<"companies"> | null
): Promise<TicketFieldScopeMap> {
const uniqueScopes = Array.from(new Set(scopes.filter((scope) => Boolean(scope))));
if (uniqueScopes.length === 0) {
return new Map();
}
const scopeSet = new Set(uniqueScopes);
const companyIdStr = companyId ? String(companyId) : null;
const result: TicketFieldScopeMap = new Map();
const allFields = await ctx.db
.query("ticketFields")
@ -495,6 +496,10 @@ async function fetchTicketFieldsByScopes(
if (!scopeSet.has(scope)) {
continue;
}
const fieldCompanyId = field.companyId ? String(field.companyId) : null;
if (fieldCompanyId && (!companyIdStr || companyIdStr !== fieldCompanyId)) {
continue;
}
const current = result.get(scope);
if (current) {
current.push(field);
@ -634,6 +639,7 @@ async function ensureTicketFormDefaultsForTenant(ctx: MutationCtx, tenantId: str
label: option.label,
})),
scope: template.key,
companyId: field.companyId ?? undefined,
order,
createdAt: now,
updatedAt: now,
@ -1319,9 +1325,6 @@ 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)));
@ -1371,7 +1374,6 @@ export const list = query({
const normalizedStatusFilter = args.status ? normalizeStatus(args.status) : null;
const normalizedPriorityFilter = normalizePriorityFilter(args.priority);
const prioritySet = normalizedPriorityFilter.length > 0 ? new Set(normalizedPriorityFilter) : null;
const primaryPriorityFilter = normalizedPriorityFilter.length === 1 ? normalizedPriorityFilter[0] : null;
const normalizedChannelFilter = args.channel ? args.channel.toUpperCase() : null;
const searchTerm = args.search?.trim().toLowerCase() ?? null;
@ -1379,80 +1381,43 @@ export const list = query({
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));
}
if (primaryPriorityFilter) {
working = working.filter((q) => q.eq(q.field("priority"), primaryPriorityFilter));
}
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!))
);
const baseQuery = 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) {
const baseQuery = applyQueryFilters(
ctx.db
.query("tickets")
.withIndex("by_tenant_assignee", (q) => q.eq("tenantId", args.tenantId).eq("assigneeId", args.assigneeId!))
);
const baseQuery = 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) {
const baseQuery = applyQueryFilters(
ctx.db
.query("tickets")
.withIndex("by_tenant_requester", (q) => q.eq("tenantId", args.tenantId).eq("requesterId", args.requesterId!))
);
const baseQuery = 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) {
const baseQuery = applyQueryFilters(
ctx.db
.query("tickets")
.withIndex("by_tenant_queue", (q) => q.eq("tenantId", args.tenantId).eq("queueId", args.queueId!))
);
const baseQuery = 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))
);
const baseQuery = 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") {
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 directQuery = 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 fallbackQuery = 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;
@ -1463,9 +1428,7 @@ export const list = query({
}
base = combined.slice(0, fetchLimit);
} else {
const baseQuery = applyQueryFilters(
ctx.db.query("tickets").withIndex("by_tenant", (q) => q.eq("tenantId", args.tenantId))
);
const baseQuery = ctx.db.query("tickets").withIndex("by_tenant", (q) => q.eq("tenantId", args.tenantId));
base = await baseQuery.order("desc").take(fetchLimit);
}
@ -2829,7 +2792,7 @@ export const listTicketForms = query({
const templates = await fetchTemplateSummaries(ctx, tenantId)
const scopes = templates.map((template) => template.key)
const fieldsByScope = await fetchTicketFieldsByScopes(ctx, tenantId, scopes)
const fieldsByScope = await fetchTicketFieldsByScopes(ctx, tenantId, scopes, viewerCompanyId)
const staffOverride = viewerRole === "ADMIN" || viewerRole === "AGENT"
const settingsByTemplate = staffOverride