feat(mentions): switch to Convex-backed search so @<ref> and text queries return visible tickets for the current user; keep permissions
This commit is contained in:
parent
e6c841383e
commit
66fe34868c
1 changed files with 62 additions and 67 deletions
|
|
@ -1,13 +1,14 @@
|
||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
|
import { ConvexHttpClient } from "convex/browser"
|
||||||
|
import { api } from "@/convex/_generated/api"
|
||||||
|
import type { Id } from "@/convex/_generated/dataModel"
|
||||||
|
import { env } from "@/lib/env"
|
||||||
import { assertAuthenticatedSession } from "@/lib/auth-server"
|
import { assertAuthenticatedSession } from "@/lib/auth-server"
|
||||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||||
import { prisma } from "@/lib/prisma"
|
|
||||||
|
|
||||||
export const runtime = "nodejs"
|
export const runtime = "nodejs"
|
||||||
|
|
||||||
const MAX_RESULTS = 10
|
const MAX_RESULTS = 10
|
||||||
const MAX_SCAN = 60
|
|
||||||
|
|
||||||
function normalizeRole(role?: string | null) {
|
function normalizeRole(role?: string | null) {
|
||||||
return (role ?? "").toLowerCase()
|
return (role ?? "").toLowerCase()
|
||||||
|
|
@ -19,10 +20,14 @@ export async function GET(request: Request) {
|
||||||
return NextResponse.json({ items: [] }, { status: 401 })
|
return NextResponse.json({ items: [] }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
|
||||||
|
if (!convexUrl) {
|
||||||
|
return NextResponse.json({ items: [] }, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedRole = normalizeRole(session.user.role)
|
const normalizedRole = normalizeRole(session.user.role)
|
||||||
const isAgentOrAdmin = normalizedRole === "admin" || normalizedRole === "agent"
|
const isAgentOrAdmin = normalizedRole === "admin" || normalizedRole === "agent"
|
||||||
const canLinkOwnTickets = normalizedRole === "collaborator"
|
const canLinkOwnTickets = normalizedRole === "collaborator"
|
||||||
|
|
||||||
if (!isAgentOrAdmin && !canLinkOwnTickets) {
|
if (!isAgentOrAdmin && !canLinkOwnTickets) {
|
||||||
return NextResponse.json({ items: [] }, { status: 403 })
|
return NextResponse.json({ items: [] }, { status: 403 })
|
||||||
}
|
}
|
||||||
|
|
@ -32,75 +37,65 @@ export async function GET(request: Request) {
|
||||||
const rawQuery = url.searchParams.get("q") ?? ""
|
const rawQuery = url.searchParams.get("q") ?? ""
|
||||||
const query = rawQuery.trim()
|
const query = rawQuery.trim()
|
||||||
|
|
||||||
const whereBase: {
|
const client = new ConvexHttpClient(convexUrl)
|
||||||
tenantId: string
|
|
||||||
requesterId?: string
|
|
||||||
} = { tenantId }
|
|
||||||
|
|
||||||
if (!isAgentOrAdmin) {
|
// Garantir que o usuário exista no Convex para obter viewerId
|
||||||
whereBase.requesterId = session.user.id
|
let viewerId: string | null = null
|
||||||
|
try {
|
||||||
|
const ensured = await client.mutation(api.users.ensureUser, {
|
||||||
|
tenantId,
|
||||||
|
name: session.user.name ?? session.user.email,
|
||||||
|
email: session.user.email,
|
||||||
|
avatarUrl: session.user.avatarUrl ?? undefined,
|
||||||
|
role: session.user.role.toUpperCase(),
|
||||||
|
})
|
||||||
|
viewerId = ensured?._id ?? null
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[mentions] ensureUser failed", error)
|
||||||
|
return NextResponse.json({ items: [] }, { status: 500 })
|
||||||
|
}
|
||||||
|
if (!viewerId) {
|
||||||
|
return NextResponse.json({ items: [] }, { status: 403 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const numericQuery = /^\d+$/.test(query)
|
// Pesquisar pelos tickets visíveis ao viewer (assunto, resumo ou #referência)
|
||||||
|
let tickets: Array<{
|
||||||
// Fast path for numeric query: exact reference match at DB level
|
id: string
|
||||||
let filtered: any[] = []
|
reference: number
|
||||||
if (numericQuery) {
|
subject: string
|
||||||
const ref = Number(query)
|
status: string
|
||||||
const exact = await prisma.ticket.findMany({
|
priority: string
|
||||||
where: { ...whereBase, reference: ref },
|
requester?: { name?: string | null } | null
|
||||||
include: {
|
assignee?: { name?: string | null } | null
|
||||||
assignee: { select: { name: true } },
|
company?: { name?: string | null } | null
|
||||||
requester: { select: { name: true } },
|
updatedAt?: number | null
|
||||||
company: { select: { name: true } },
|
}> = []
|
||||||
},
|
try {
|
||||||
take: MAX_RESULTS,
|
const res = (await client.query(api.tickets.list, {
|
||||||
})
|
tenantId,
|
||||||
filtered = exact
|
viewerId: viewerId as unknown as Id<"users">,
|
||||||
} else {
|
search: query,
|
||||||
const tickets = await prisma.ticket.findMany({
|
limit: 40,
|
||||||
where: whereBase,
|
})) as any[]
|
||||||
include: {
|
tickets = Array.isArray(res) ? res : []
|
||||||
assignee: { select: { name: true } },
|
} catch (error) {
|
||||||
requester: { select: { name: true } },
|
console.error("[mentions] tickets.list failed", error)
|
||||||
company: { select: { name: true } },
|
return NextResponse.json({ items: [] }, { status: 500 })
|
||||||
},
|
|
||||||
orderBy: { updatedAt: "desc" },
|
|
||||||
take: MAX_SCAN,
|
|
||||||
})
|
|
||||||
|
|
||||||
const lowered = query.toLowerCase()
|
|
||||||
filtered = tickets
|
|
||||||
.filter((ticket) => {
|
|
||||||
if (!query) return true
|
|
||||||
const subject = ticket.subject ?? ""
|
|
||||||
if (subject.toLowerCase().includes(lowered)) return true
|
|
||||||
const requesterName = ticket.requester?.name ?? ""
|
|
||||||
if (requesterName.toLowerCase().includes(lowered)) return true
|
|
||||||
const companyName = ticket.company?.name ?? ""
|
|
||||||
if (companyName.toLowerCase().includes(lowered)) return true
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
.slice(0, MAX_RESULTS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const basePath = isAgentOrAdmin ? "/tickets" : "/portal/tickets"
|
const basePath = isAgentOrAdmin ? "/tickets" : "/portal/tickets"
|
||||||
|
const items = tickets.slice(0, MAX_RESULTS).map((t) => ({
|
||||||
const items = filtered.map((ticket) => {
|
id: String(t.id),
|
||||||
const subject = ticket.subject ?? ""
|
reference: Number(t.reference),
|
||||||
return {
|
subject: String(t.subject ?? ""),
|
||||||
id: ticket.id,
|
status: String(t.status ?? "PENDING"),
|
||||||
reference: ticket.reference,
|
priority: String(t.priority ?? "MEDIUM"),
|
||||||
subject,
|
requesterName: t.requester?.name ?? null,
|
||||||
status: ticket.status,
|
assigneeName: t.assignee?.name ?? null,
|
||||||
priority: ticket.priority,
|
companyName: t.company?.name ?? null,
|
||||||
requesterName: ticket.requester?.name ?? null,
|
url: `${basePath}/${String(t.id)}`,
|
||||||
assigneeName: ticket.assignee?.name ?? null,
|
updatedAt: t.updatedAt ? new Date(t.updatedAt).toISOString() : new Date().toISOString(),
|
||||||
companyName: ticket.company?.name ?? null,
|
}))
|
||||||
url: `${basePath}/${ticket.id}`,
|
|
||||||
updatedAt: ticket.updatedAt.toISOString(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({ items })
|
return NextResponse.json({ items })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue