chore: prep platform improvements
This commit is contained in:
parent
a62f3d5283
commit
c5ddd54a3e
24 changed files with 777 additions and 649 deletions
|
|
@ -412,8 +412,7 @@ export const deleteCategory = mutation({
|
|||
}
|
||||
const ticketsToMove = await ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||
.filter((q) => q.eq(q.field("categoryId"), categoryId))
|
||||
.withIndex("by_tenant_category", (q) => q.eq("tenantId", tenantId).eq("categoryId", categoryId))
|
||||
.collect()
|
||||
for (const ticket of ticketsToMove) {
|
||||
await ctx.db.patch(ticket._id, {
|
||||
|
|
@ -425,8 +424,7 @@ export const deleteCategory = mutation({
|
|||
} else {
|
||||
const ticketsLinked = await ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||
.filter((q) => q.eq(q.field("categoryId"), categoryId))
|
||||
.withIndex("by_tenant_category", (q) => q.eq("tenantId", tenantId).eq("categoryId", categoryId))
|
||||
.first()
|
||||
if (ticketsLinked) {
|
||||
throw new ConvexError("Não é possível remover uma categoria vinculada a tickets sem informar destino")
|
||||
|
|
@ -526,8 +524,7 @@ export const deleteSubcategory = mutation({
|
|||
}
|
||||
const tickets = await ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||
.filter((q) => q.eq(q.field("subcategoryId"), subcategoryId))
|
||||
.withIndex("by_tenant_subcategory", (q) => q.eq("tenantId", tenantId).eq("subcategoryId", subcategoryId))
|
||||
.collect()
|
||||
for (const ticket of tickets) {
|
||||
await ctx.db.patch(ticket._id, {
|
||||
|
|
@ -538,8 +535,7 @@ export const deleteSubcategory = mutation({
|
|||
} else {
|
||||
const linked = await ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||
.filter((q) => q.eq(q.field("subcategoryId"), subcategoryId))
|
||||
.withIndex("by_tenant_subcategory", (q) => q.eq("tenantId", tenantId).eq("subcategoryId", subcategoryId))
|
||||
.first()
|
||||
if (linked) {
|
||||
throw new ConvexError("Não é possível remover uma subcategoria vinculada a tickets sem informar destino")
|
||||
|
|
|
|||
|
|
@ -37,6 +37,15 @@ function validateOptions(type: FieldType, options: { value: string; label: strin
|
|||
}
|
||||
}
|
||||
|
||||
async function validateCompanyScope(ctx: AnyCtx, tenantId: string, companyId?: Id<"companies"> | null) {
|
||||
if (!companyId) return undefined;
|
||||
const company = await ctx.db.get(companyId);
|
||||
if (!company || company.tenantId !== tenantId) {
|
||||
throw new ConvexError("Empresa inválida para o campo");
|
||||
}
|
||||
return companyId;
|
||||
}
|
||||
|
||||
export const list = query({
|
||||
args: { tenantId: v.string(), viewerId: v.id("users"), scope: v.optional(v.string()) },
|
||||
handler: async (ctx, { tenantId, viewerId, scope }) => {
|
||||
|
|
@ -64,6 +73,7 @@ export const list = query({
|
|||
options: field.options ?? [],
|
||||
order: field.order,
|
||||
scope: field.scope ?? "all",
|
||||
companyId: field.companyId ?? null,
|
||||
createdAt: field.createdAt,
|
||||
updatedAt: field.updatedAt,
|
||||
}));
|
||||
|
|
@ -97,6 +107,7 @@ export const listForTenant = query({
|
|||
options: field.options ?? [],
|
||||
order: field.order,
|
||||
scope: field.scope ?? "all",
|
||||
companyId: field.companyId ?? null,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
|
@ -118,8 +129,9 @@ export const create = mutation({
|
|||
)
|
||||
),
|
||||
scope: v.optional(v.string()),
|
||||
companyId: v.optional(v.id("companies")),
|
||||
},
|
||||
handler: async (ctx, { tenantId, actorId, label, description, type, required, options, scope }) => {
|
||||
handler: async (ctx, { tenantId, actorId, label, description, type, required, options, scope, companyId }) => {
|
||||
await requireAdmin(ctx, actorId, tenantId);
|
||||
const normalizedLabel = label.trim();
|
||||
if (normalizedLabel.length < 2) {
|
||||
|
|
@ -140,6 +152,7 @@ export const create = mutation({
|
|||
}
|
||||
return safe;
|
||||
})();
|
||||
const companyRef = await validateCompanyScope(ctx, tenantId, companyId ?? undefined);
|
||||
|
||||
const existing = await ctx.db
|
||||
.query("ticketFields")
|
||||
|
|
@ -158,6 +171,7 @@ export const create = mutation({
|
|||
options,
|
||||
order: maxOrder + 1,
|
||||
scope: normalizedScope,
|
||||
companyId: companyRef,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
|
@ -183,8 +197,9 @@ export const update = mutation({
|
|||
)
|
||||
),
|
||||
scope: v.optional(v.string()),
|
||||
companyId: v.optional(v.id("companies")),
|
||||
},
|
||||
handler: async (ctx, { tenantId, fieldId, actorId, label, description, type, required, options, scope }) => {
|
||||
handler: async (ctx, { tenantId, fieldId, actorId, label, description, type, required, options, scope, companyId }) => {
|
||||
await requireAdmin(ctx, actorId, tenantId);
|
||||
const field = await ctx.db.get(fieldId);
|
||||
if (!field || field.tenantId !== tenantId) {
|
||||
|
|
@ -208,6 +223,7 @@ export const update = mutation({
|
|||
}
|
||||
return safe;
|
||||
})();
|
||||
const companyRef = await validateCompanyScope(ctx, tenantId, companyId ?? undefined);
|
||||
|
||||
let key = field.key;
|
||||
if (field.label !== normalizedLabel) {
|
||||
|
|
@ -223,6 +239,7 @@ export const update = mutation({
|
|||
required,
|
||||
options,
|
||||
scope: normalizedScope,
|
||||
companyId: companyRef,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -211,8 +211,7 @@ async function ensureQueue(
|
|||
|
||||
const byName = await ctx.db
|
||||
.query("queues")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||
.filter((q) => q.eq(q.field("name"), data.name))
|
||||
.withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId).eq("name", data.name))
|
||||
.first()
|
||||
if (byName) {
|
||||
if (byName.slug !== slug) {
|
||||
|
|
|
|||
|
|
@ -67,8 +67,7 @@ async function ensureUniqueSlug(ctx: AnyCtx, tenantId: string, slug: string, exc
|
|||
async function ensureUniqueName(ctx: AnyCtx, tenantId: string, name: string, excludeId?: Id<"queues">) {
|
||||
const existing = await ctx.db
|
||||
.query("queues")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||
.filter((q) => q.eq(q.field("name"), name))
|
||||
.withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId).eq("name", name))
|
||||
.first();
|
||||
if (existing && (!excludeId || existing._id !== excludeId)) {
|
||||
throw new ConvexError("Já existe uma fila com este nome");
|
||||
|
|
|
|||
|
|
@ -184,6 +184,8 @@ export default defineSchema({
|
|||
teamId: v.optional(v.id("teams")),
|
||||
})
|
||||
.index("by_tenant_slug", ["tenantId", "slug"])
|
||||
.index("by_tenant_team", ["tenantId", "teamId"])
|
||||
.index("by_tenant_name", ["tenantId", "name"])
|
||||
.index("by_tenant", ["tenantId"]),
|
||||
|
||||
teams: defineTable({
|
||||
|
|
@ -322,6 +324,9 @@ export default defineSchema({
|
|||
.index("by_tenant_requester", ["tenantId", "requesterId"])
|
||||
.index("by_tenant_company", ["tenantId", "companyId"])
|
||||
.index("by_tenant_machine", ["tenantId", "machineId"])
|
||||
.index("by_tenant_category", ["tenantId", "categoryId"])
|
||||
.index("by_tenant_subcategory", ["tenantId", "subcategoryId"])
|
||||
.index("by_tenant_sla_policy", ["tenantId", "slaPolicyId"])
|
||||
.index("by_tenant", ["tenantId"])
|
||||
.index("by_tenant_created", ["tenantId", "createdAt"])
|
||||
.index("by_tenant_resolved", ["tenantId", "resolvedAt"])
|
||||
|
|
@ -480,6 +485,7 @@ export default defineSchema({
|
|||
key: v.string(),
|
||||
label: v.string(),
|
||||
type: v.string(),
|
||||
companyId: v.optional(v.id("companies")),
|
||||
description: v.optional(v.string()),
|
||||
required: v.boolean(),
|
||||
order: v.number(),
|
||||
|
|
@ -498,6 +504,7 @@ export default defineSchema({
|
|||
.index("by_tenant_key", ["tenantId", "key"])
|
||||
.index("by_tenant_order", ["tenantId", "order"])
|
||||
.index("by_tenant_scope", ["tenantId", "scope"])
|
||||
.index("by_tenant_company", ["tenantId", "companyId"])
|
||||
.index("by_tenant", ["tenantId"]),
|
||||
|
||||
ticketFormSettings: defineTable({
|
||||
|
|
|
|||
|
|
@ -126,8 +126,7 @@ export const remove = mutation({
|
|||
|
||||
const ticketLinked = await ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||
.filter((q) => q.eq(q.field("slaPolicyId"), policyId))
|
||||
.withIndex("by_tenant_sla_policy", (q) => q.eq("tenantId", tenantId).eq("slaPolicyId", policyId))
|
||||
.first();
|
||||
if (ticketLinked) {
|
||||
throw new ConvexError("Remova a associação de tickets antes de excluir a política");
|
||||
|
|
|
|||
|
|
@ -141,8 +141,7 @@ export const remove = mutation({
|
|||
|
||||
const queuesLinked = await ctx.db
|
||||
.query("queues")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||
.filter((q) => q.eq(q.field("teamId"), teamId))
|
||||
.withIndex("by_tenant_team", (q) => q.eq("tenantId", tenantId).eq("teamId", teamId))
|
||||
.first();
|
||||
if (queuesLinked) {
|
||||
throw new ConvexError("Remova ou realoque as filas associadas antes de excluir o time");
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ async function cloneFieldsFromTemplate(ctx: MutationCtx, tenantId: string, sourc
|
|||
required: field.required,
|
||||
options: field.options ?? undefined,
|
||||
scope: targetKey,
|
||||
companyId: field.companyId ?? undefined,
|
||||
order,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use server";
|
||||
|
||||
import type { Id } from "./_generated/dataModel";
|
||||
|
||||
export type TicketFormFieldSeed = {
|
||||
key: string;
|
||||
label: string;
|
||||
|
|
@ -7,6 +9,7 @@ export type TicketFormFieldSeed = {
|
|||
required?: boolean;
|
||||
description?: string;
|
||||
options?: Array<{ value: string; label: string }>;
|
||||
companyId?: Id<"companies"> | null;
|
||||
};
|
||||
|
||||
export const TICKET_FORM_CONFIG = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue