230 lines
6.3 KiB
TypeScript
230 lines
6.3 KiB
TypeScript
'use server'
|
|
|
|
import { PERMS } from '@/lib/permissions'
|
|
import { prisma } from '@/lib/prisma'
|
|
import { adminAction, authAction } from '@/lib/safe-action'
|
|
import { ActionError, actionOk } from '@/lib/safe-action-shared'
|
|
import { logAudit } from '@/lib/services/audit'
|
|
import { notify } from '@/lib/services/webhook'
|
|
import {
|
|
assignTicketSchema,
|
|
createTicketSchema,
|
|
replyTicketSchema,
|
|
updateTicketPrioritySchema,
|
|
updateTicketStatusSchema,
|
|
} from '@/lib/validators/ticket'
|
|
|
|
// ── User actions (authenticated, no admin perms needed) ──────────────
|
|
|
|
export const createTicket = authAction({ schema: createTicketSchema }, async (ctx) => {
|
|
const ticket = await prisma.websiteTicket.create({
|
|
data: {
|
|
subject: ctx.data.subject,
|
|
category: ctx.data.category,
|
|
creatorId: ctx.session.user.id,
|
|
},
|
|
})
|
|
|
|
// Create the first message
|
|
await prisma.websiteTicketMessage.create({
|
|
data: {
|
|
ticketId: ticket.id,
|
|
userId: ctx.session.user.id,
|
|
message: ctx.data.message,
|
|
isStaff: 0,
|
|
},
|
|
})
|
|
|
|
notify({
|
|
action: 'ticket_create',
|
|
actor: ctx.session.user.username,
|
|
target: `#${ticket.id} - ${ticket.subject}`,
|
|
details: `Category: ${ticket.category}`,
|
|
})
|
|
|
|
return actionOk({ id: ticket.id })
|
|
})
|
|
|
|
export const userReplyTicket = authAction({ schema: replyTicketSchema }, async (ctx) => {
|
|
const ticket = await prisma.websiteTicket.findUnique({
|
|
where: { id: ctx.data.ticketId },
|
|
})
|
|
|
|
if (!ticket) throw new ActionError('Ticket not found')
|
|
if (ticket.creatorId !== ctx.session.user.id) throw new ActionError('Unauthorized')
|
|
if (ticket.status === 'closed') throw new ActionError('Ticket is closed')
|
|
|
|
await prisma.websiteTicketMessage.create({
|
|
data: {
|
|
ticketId: ctx.data.ticketId,
|
|
userId: ctx.session.user.id,
|
|
message: ctx.data.message,
|
|
isStaff: 0,
|
|
},
|
|
})
|
|
|
|
// If ticket was in "waiting" (waiting for user), move back to open
|
|
if (ticket.status === 'waiting') {
|
|
await prisma.websiteTicket.update({
|
|
where: { id: ctx.data.ticketId },
|
|
data: { status: 'open' },
|
|
})
|
|
}
|
|
|
|
return actionOk()
|
|
})
|
|
|
|
export const closeTicketByUser = authAction(
|
|
{ schema: replyTicketSchema.pick({ ticketId: true }) },
|
|
async (ctx) => {
|
|
const ticket = await prisma.websiteTicket.findUnique({
|
|
where: { id: ctx.data.ticketId },
|
|
})
|
|
|
|
if (!ticket) throw new ActionError('Ticket not found')
|
|
if (ticket.creatorId !== ctx.session.user.id) throw new ActionError('Unauthorized')
|
|
if (ticket.status === 'closed') throw new ActionError('Ticket is already closed')
|
|
|
|
await prisma.websiteTicket.update({
|
|
where: { id: ctx.data.ticketId },
|
|
data: { status: 'closed', closedAt: new Date() },
|
|
})
|
|
|
|
return actionOk()
|
|
},
|
|
)
|
|
|
|
// ── Admin actions ────────────────────────────────────────────────────
|
|
|
|
export const adminReplyTicket = adminAction(
|
|
{ permission: PERMS.TICKETS_EDIT, schema: replyTicketSchema },
|
|
async (ctx) => {
|
|
const ticket = await prisma.websiteTicket.findUnique({
|
|
where: { id: ctx.data.ticketId },
|
|
})
|
|
|
|
if (!ticket) throw new ActionError('Ticket not found')
|
|
|
|
await prisma.websiteTicketMessage.create({
|
|
data: {
|
|
ticketId: ctx.data.ticketId,
|
|
userId: ctx.session.user.id,
|
|
message: ctx.data.message,
|
|
isStaff: 1,
|
|
},
|
|
})
|
|
|
|
// Auto-assign if not assigned yet
|
|
const updates: Record<string, unknown> = { status: 'waiting' }
|
|
if (!ticket.assigneeId) {
|
|
updates.assigneeId = ctx.session.user.id
|
|
}
|
|
|
|
await prisma.websiteTicket.update({
|
|
where: { id: ctx.data.ticketId },
|
|
data: updates,
|
|
})
|
|
|
|
logAudit({
|
|
userId: ctx.session.user.id,
|
|
action: 'ticket_reply',
|
|
target: 'WebsiteTicket',
|
|
targetId: ctx.data.ticketId,
|
|
})
|
|
|
|
return actionOk()
|
|
},
|
|
)
|
|
|
|
export const updateTicketStatus = adminAction(
|
|
{ permission: PERMS.TICKETS_EDIT, schema: updateTicketStatusSchema },
|
|
async (ctx) => {
|
|
const ticket = await prisma.websiteTicket.findUnique({
|
|
where: { id: ctx.data.ticketId },
|
|
})
|
|
|
|
if (!ticket) throw new ActionError('Ticket not found')
|
|
|
|
const data: Record<string, unknown> = { status: ctx.data.status }
|
|
if (ctx.data.status === 'closed') {
|
|
data.closedAt = new Date()
|
|
}
|
|
if (ctx.data.status === 'in_progress' && !ticket.assigneeId) {
|
|
data.assigneeId = ctx.session.user.id
|
|
}
|
|
|
|
await prisma.websiteTicket.update({
|
|
where: { id: ctx.data.ticketId },
|
|
data,
|
|
})
|
|
|
|
logAudit({
|
|
userId: ctx.session.user.id,
|
|
action: 'ticket_status_change',
|
|
target: 'WebsiteTicket',
|
|
targetId: ctx.data.ticketId,
|
|
before: { status: ticket.status },
|
|
after: { status: ctx.data.status },
|
|
})
|
|
|
|
return actionOk()
|
|
},
|
|
)
|
|
|
|
export const assignTicket = adminAction(
|
|
{ permission: PERMS.TICKETS_EDIT, schema: assignTicketSchema },
|
|
async (ctx) => {
|
|
const ticket = await prisma.websiteTicket.findUnique({
|
|
where: { id: ctx.data.ticketId },
|
|
})
|
|
|
|
if (!ticket) throw new ActionError('Ticket not found')
|
|
|
|
await prisma.websiteTicket.update({
|
|
where: { id: ctx.data.ticketId },
|
|
data: {
|
|
assigneeId: ctx.data.assigneeId,
|
|
status: ctx.data.assigneeId ? 'in_progress' : 'open',
|
|
},
|
|
})
|
|
|
|
logAudit({
|
|
userId: ctx.session.user.id,
|
|
action: 'ticket_assign',
|
|
target: 'WebsiteTicket',
|
|
targetId: ctx.data.ticketId,
|
|
before: { assigneeId: ticket.assigneeId },
|
|
after: { assigneeId: ctx.data.assigneeId },
|
|
})
|
|
|
|
return actionOk()
|
|
},
|
|
)
|
|
|
|
export const updateTicketPriority = adminAction(
|
|
{ permission: PERMS.TICKETS_EDIT, schema: updateTicketPrioritySchema },
|
|
async (ctx) => {
|
|
const ticket = await prisma.websiteTicket.findUnique({
|
|
where: { id: ctx.data.ticketId },
|
|
})
|
|
|
|
if (!ticket) throw new ActionError('Ticket not found')
|
|
|
|
await prisma.websiteTicket.update({
|
|
where: { id: ctx.data.ticketId },
|
|
data: { priority: ctx.data.priority },
|
|
})
|
|
|
|
logAudit({
|
|
userId: ctx.session.user.id,
|
|
action: 'ticket_priority_change',
|
|
target: 'WebsiteTicket',
|
|
targetId: ctx.data.ticketId,
|
|
before: { priority: ticket.priority },
|
|
after: { priority: ctx.data.priority },
|
|
})
|
|
|
|
return actionOk()
|
|
},
|
|
)
|