JUNOHUB-test/src/app/api/juno-engine/tickets/route.ts

162 lines
5.1 KiB
TypeScript

import { NextResponse } from "next/server"
import { auth } from "@/auth"
import { isVendorPortalRole } from "@/lib/auth-roles"
import { prisma } from "@/lib/prisma"
import { logger } from "@/lib/logger"
// Normalize subject by stripping common reply prefixes so we can group conversations
function normalizeSubject(subject: string | null): string {
if (!subject) return "No subject"
let s = subject.trim()
// Remove repeated "Re:" / "Fwd:" prefixes
// e.g. "Re: Re: Hello" -> "Hello"
// Case-insensitive, allow localized variants in future by trimming until no prefix matches
while (true) {
const match = s.match(/^(re|fw|fwd):\s*/i)
if (!match) break
s = s.slice(match[0].length).trim()
}
return s || "No subject"
}
export async function GET() {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ message: "Unauthorized" }, { status: 401 })
}
if (!isVendorPortalRole(session.user.role)) {
return NextResponse.json({ message: "Forbidden" }, { status: 403 })
}
// Find the vendor's active store to scope tickets
const user = await prisma.user.findUnique({
where: { id: session.user.id },
include: {
stores: {
where: { isActive: true },
take: 1,
select: { id: true }
}
}
})
const activeStore = user?.stores[0]
if (!activeStore) {
return NextResponse.json({ tickets: [] })
}
// Use a raw query instead of relying on the generated Prisma model,
// to avoid issues if the EmailLog model is not present on the client.
const logs = await prisma.$queryRaw<
Array<{
id: string
"userId": string
"storeId": string | null
to: string
subject: string | null
"bodyPreview": string | null
"isAutomated": boolean
trigger: string | null
status: string
"providerMessageId": string | null
"errorMessage": string | null
"sentAt": Date
}>
>`
SELECT
"id",
"userId",
"storeId",
"to",
"subject",
"bodyPreview",
"isAutomated",
"trigger",
"status",
"providerMessageId",
"errorMessage",
"sentAt"
FROM "EmailLog"
WHERE "storeId" = ${activeStore.id}
AND (
"isAutomated" = TRUE
OR "trigger" = 'GMAIL_INCOMING'
)
ORDER BY "sentAt" DESC
LIMIT 200
`
type TicketMessage = {
id: string
subject: string
bodyPreview: string | null
status: string
trigger: string | null
sentAt: string
}
type Ticket = {
id: string
subject: string
rootSubject: string
customerEmail: string
status: string
updatedAt: string
messagesCount: number
lastMessageSnippet: string
messages: TicketMessage[]
}
const ticketsMap = new Map<string, Ticket>()
for (const log of logs) {
const customerEmail = (log.to || "").toLowerCase()
const rootSubject = normalizeSubject(log.subject)
const key = `${customerEmail}::${rootSubject}`
const baseMessage: TicketMessage = {
id: log.id,
subject: log.subject || rootSubject,
bodyPreview: log.bodyPreview,
status: log.status,
trigger: log.trigger,
sentAt: log.sentAt.toISOString(),
}
const existing = ticketsMap.get(key)
if (!existing) {
ticketsMap.set(key, {
id: key,
subject: log.subject || rootSubject,
rootSubject,
customerEmail,
status: log.status,
updatedAt: log.sentAt.toISOString(),
messagesCount: 1,
lastMessageSnippet: log.bodyPreview || "",
messages: [baseMessage],
})
} else {
existing.messages.push(baseMessage)
existing.messagesCount += 1
// The logs are ordered newest-first, so the first seen message is the latest
// Keep lastMessageSnippet / updatedAt from the first entry
}
}
const tickets = Array.from(ticketsMap.values())
return NextResponse.json({ tickets })
} catch (error) {
logger.error("Failed to load vendor tickets from email logs", error)
return NextResponse.json(
{ message: "Failed to load tickets", tickets: [] },
{ status: 500 }
)
}
}