feat: add company management and manager role support

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
esdrasrenan 2025-10-06 21:26:43 -03:00
parent 409cbea7b9
commit 854887f499
16 changed files with 955 additions and 126 deletions

View file

@ -1,6 +1,6 @@
import { query } from "./_generated/server";
import type { QueryCtx } from "./_generated/server";
import { v } from "convex/values";
import { ConvexError, v } from "convex/values";
import type { Doc, Id } from "./_generated/dataModel";
import { requireStaff } from "./rbac";
@ -42,6 +42,25 @@ async function fetchTickets(ctx: QueryCtx, tenantId: string) {
.collect();
}
async function fetchScopedTickets(
ctx: QueryCtx,
tenantId: string,
viewer: Awaited<ReturnType<typeof requireStaff>>,
) {
if (viewer.role === "MANAGER") {
if (!viewer.user.companyId) {
throw new ConvexError("Gestor não possui empresa vinculada");
}
return ctx.db
.query("tickets")
.withIndex("by_tenant_company", (q) =>
q.eq("tenantId", tenantId).eq("companyId", viewer.user.companyId!)
)
.collect();
}
return fetchTickets(ctx, tenantId);
}
async function fetchQueues(ctx: QueryCtx, tenantId: string) {
return ctx.db
.query("queues")
@ -92,8 +111,8 @@ function formatDateKey(timestamp: number) {
export const slaOverview = query({
args: { tenantId: v.string(), viewerId: v.id("users") },
handler: async (ctx, { tenantId, viewerId }) => {
await requireStaff(ctx, viewerId, tenantId);
const tickets = await fetchTickets(ctx, tenantId);
const viewer = await requireStaff(ctx, viewerId, tenantId);
const tickets = await fetchScopedTickets(ctx, tenantId, viewer);
const queues = await fetchQueues(ctx, tenantId);
const now = Date.now();
@ -140,8 +159,8 @@ export const slaOverview = query({
export const csatOverview = query({
args: { tenantId: v.string(), viewerId: v.id("users") },
handler: async (ctx, { tenantId, viewerId }) => {
await requireStaff(ctx, viewerId, tenantId);
const tickets = await fetchTickets(ctx, tenantId);
const viewer = await requireStaff(ctx, viewerId, tenantId);
const tickets = await fetchScopedTickets(ctx, tenantId, viewer);
const surveys = await collectCsatSurveys(ctx, tickets);
const averageScore = average(surveys.map((item) => item.score));
@ -171,8 +190,8 @@ export const csatOverview = query({
export const backlogOverview = query({
args: { tenantId: v.string(), viewerId: v.id("users") },
handler: async (ctx, { tenantId, viewerId }) => {
await requireStaff(ctx, viewerId, tenantId);
const tickets = await fetchTickets(ctx, tenantId);
const viewer = await requireStaff(ctx, viewerId, tenantId);
const tickets = await fetchScopedTickets(ctx, tenantId, viewer);
const statusCounts = tickets.reduce<Record<string, number>>((acc, ticket) => {
acc[ticket.status] = (acc[ticket.status] ?? 0) + 1;
@ -218,8 +237,8 @@ export const backlogOverview = query({
export const dashboardOverview = query({
args: { tenantId: v.string(), viewerId: v.id("users") },
handler: async (ctx, { tenantId, viewerId }) => {
await requireStaff(ctx, viewerId, tenantId);
const tickets = await fetchTickets(ctx, tenantId);
const viewer = await requireStaff(ctx, viewerId, tenantId);
const tickets = await fetchScopedTickets(ctx, tenantId, viewer);
const now = Date.now();
const lastDayStart = now - ONE_DAY_MS;
@ -294,8 +313,8 @@ export const ticketsByChannel = query({
range: v.optional(v.string()),
},
handler: async (ctx, { tenantId, viewerId, range }) => {
await requireStaff(ctx, viewerId, tenantId);
const tickets = await fetchTickets(ctx, tenantId);
const viewer = await requireStaff(ctx, viewerId, tenantId);
const tickets = await fetchScopedTickets(ctx, tenantId, viewer);
const days = range === "7d" ? 7 : range === "30d" ? 30 : 90;
const end = new Date();