This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
+178
-169
@@ -7,223 +7,232 @@ 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,
|
||||
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,
|
||||
},
|
||||
});
|
||||
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,
|
||||
},
|
||||
});
|
||||
// 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}`,
|
||||
});
|
||||
notify({
|
||||
action: "ticket_create",
|
||||
actor: ctx.session.user.username,
|
||||
target: `#${ticket.id} - ${ticket.subject}`,
|
||||
details: `Category: ${ticket.category}`,
|
||||
});
|
||||
|
||||
return actionOk({ id: ticket.id });
|
||||
});
|
||||
return actionOk({ id: ticket.id });
|
||||
},
|
||||
);
|
||||
|
||||
export const userReplyTicket = authAction({ schema: replyTicketSchema }, async (ctx) => {
|
||||
const ticket = await prisma.websiteTicket.findUnique({
|
||||
where: { id: ctx.data.ticketId },
|
||||
});
|
||||
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");
|
||||
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,
|
||||
},
|
||||
});
|
||||
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" },
|
||||
});
|
||||
}
|
||||
// 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();
|
||||
});
|
||||
return actionOk();
|
||||
},
|
||||
);
|
||||
|
||||
export const closeTicketByUser = authAction(
|
||||
{ schema: replyTicketSchema.pick({ ticketId: true }) },
|
||||
async (ctx) => {
|
||||
const ticket = await prisma.websiteTicket.findUnique({
|
||||
where: { id: ctx.data.ticketId },
|
||||
});
|
||||
{ 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");
|
||||
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() },
|
||||
});
|
||||
await prisma.websiteTicket.update({
|
||||
where: { id: ctx.data.ticketId },
|
||||
data: { status: "closed", closedAt: new Date() },
|
||||
});
|
||||
|
||||
return actionOk();
|
||||
},
|
||||
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 },
|
||||
});
|
||||
{ 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");
|
||||
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,
|
||||
},
|
||||
});
|
||||
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;
|
||||
}
|
||||
// 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,
|
||||
});
|
||||
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,
|
||||
});
|
||||
logAudit({
|
||||
userId: ctx.session.user.id,
|
||||
action: "ticket_reply",
|
||||
target: "WebsiteTicket",
|
||||
targetId: ctx.data.ticketId,
|
||||
});
|
||||
|
||||
return actionOk();
|
||||
},
|
||||
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 },
|
||||
});
|
||||
{ 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");
|
||||
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;
|
||||
}
|
||||
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,
|
||||
});
|
||||
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 },
|
||||
});
|
||||
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();
|
||||
},
|
||||
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 },
|
||||
});
|
||||
{ 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");
|
||||
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",
|
||||
},
|
||||
});
|
||||
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 },
|
||||
});
|
||||
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();
|
||||
},
|
||||
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 },
|
||||
});
|
||||
{ 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");
|
||||
if (!ticket) throw new ActionError("Ticket not found");
|
||||
|
||||
await prisma.websiteTicket.update({
|
||||
where: { id: ctx.data.ticketId },
|
||||
data: { priority: ctx.data.priority },
|
||||
});
|
||||
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 },
|
||||
});
|
||||
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();
|
||||
},
|
||||
return actionOk();
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user