diff --git a/src/models/ticket.ts b/src/models/ticket.ts index 51db565..27ccbb3 100644 --- a/src/models/ticket.ts +++ b/src/models/ticket.ts @@ -14,3 +14,9 @@ export const UpdateTicketSchema = z.object({ status: z.string().min(1).optional(), owner_id: z.string().uuid().optional(), }); + +export const CommentSchema = z.object({ + body: z.string().min(1), + creator_id: z.string().optional().default('00000000-0000-0000-0000-000000000000'), + internal: z.boolean().optional().default(false), +}); diff --git a/src/routes/tickets.ts b/src/routes/tickets.ts index cb75795..9895612 100644 --- a/src/routes/tickets.ts +++ b/src/routes/tickets.ts @@ -3,7 +3,7 @@ import { HTTPException } from 'hono/http-exception'; import type { Db } from '../db/index.ts'; import { tickets, transactions, customFieldValues, customFields, queues, lifecycles } from '../db/schema.ts'; import { eq, asc } from 'drizzle-orm'; -import { CreateTicketSchema, UpdateTicketSchema } from '../models/ticket.ts'; +import { CreateTicketSchema, UpdateTicketSchema, CommentSchema } from '../models/ticket.ts'; import { ScripEngine } from '../scrip/engine.ts'; import { LifecycleValidator } from '../lifecycle/validator.ts'; import type { LifecycleDefinition } from '../lifecycle/validator.ts'; @@ -231,5 +231,40 @@ export function createTicketsRouter(db: Db): Hono { return c.json(result); }); + // POST /:id/comment — add a comment (reply or internal note) + router.post('/:id/comment', async (c) => { + const id = Number(c.req.param('id')); + const body = await c.req.json(); + const parsed = CommentSchema.parse(body); + + const ticket = await db.query.tickets.findFirst({ + where: eq(tickets.id, id), + }); + + if (!ticket) { + throw new HTTPException(404, { message: 'Ticket not found' }); + } + + const transactionType = parsed.internal ? 'Comment' : 'Correspond'; + + const [tx] = await db.insert(transactions).values({ + ticket_id: id, + transaction_type: transactionType, + data: { body: parsed.body }, + creator_id: parsed.creator_id, + }).returning(); + + if (!tx) { + throw new HTTPException(500, { message: 'Failed to create comment' }); + } + + // Run scrips + const txList = [tx]; + const prepared = await scripEngine.prepare(id, txList as any); + await scripEngine.commit(prepared); + + return c.json(tx, 201); + }); + return router; } diff --git a/web/src/app/tickets/[id]/page.tsx b/web/src/app/tickets/[id]/page.tsx index 67514ea..65362e0 100644 --- a/web/src/app/tickets/[id]/page.tsx +++ b/web/src/app/tickets/[id]/page.tsx @@ -11,6 +11,7 @@ import { getQueues, previewTicket, updateTicket, + sendComment, } from "@/lib/api"; import type { Ticket, @@ -164,6 +165,8 @@ export default function TicketDetailPage({ // Reply const [replyText, setReplyText] = useState(""); const [replyMode, setReplyMode] = useState<"public" | "internal">("public"); + const [sending, setSending] = useState(false); + const [sendError, setSendError] = useState(null); // Status change const [statusSelectOpen, setStatusSelectOpen] = useState(false); @@ -258,6 +261,28 @@ export default function TicketDetailPage({ setScripResults(null); }; + const handleSendComment = async () => { + if (!replyText.trim() || sending) return; + setSending(true); + setSendError(null); + + const { error } = await sendComment(id, { + body: replyText.trim(), + internal: replyMode === "internal", + }); + + setSending(false); + + if (error) { + setSendError(error); + } else { + setReplyText(""); + setSendError(null); + const txRes = await getTicketTransactions(id); + if (txRes.data) setTransactions(txRes.data); + } + }; + if (loading) { return (
@@ -378,7 +403,10 @@ export default function TicketDetailPage({