Implement ticket reply functionality
Backend:
- POST /:id/comment endpoint accepting {body, internal?, creator_id?}
- internal=false → Correspond (public reply), internal=true → Comment
- Runs scrip engine on the new transaction so notifications fire
- CommentSchema zod validation
Frontend:
- sendComment() API function in lib/api.ts
- Send button wired with onClick, sending spinner, disabled state
- Error display below reply box, clears on new typing
- Refreshes transaction list after successful send
- Reply/Internal note mode passed as internal flag
This commit is contained in:
@@ -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<string | null>(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 (
|
||||
<div className="flex h-full">
|
||||
@@ -378,7 +403,10 @@ export default function TicketDetailPage({
|
||||
<div className="flex items-end gap-2">
|
||||
<textarea
|
||||
value={replyText}
|
||||
onChange={(e) => setReplyText(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setReplyText(e.target.value);
|
||||
setSendError(null);
|
||||
}}
|
||||
placeholder="Reply to this ticket..."
|
||||
rows={2}
|
||||
className="flex-1 px-3 py-2 rounded-lg bg-background border border-border text-sm text-foreground placeholder:text-muted-foreground outline-none focus:border-primary focus:ring-1 focus:ring-primary resize-none transition-all duration-150"
|
||||
@@ -388,18 +416,30 @@ export default function TicketDetailPage({
|
||||
<PaperclipIcon className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
disabled={!replyText.trim()}
|
||||
onClick={handleSendComment}
|
||||
disabled={!replyText.trim() || sending}
|
||||
className={cn(
|
||||
"w-8 h-8 flex items-center justify-center rounded-lg transition-all duration-150",
|
||||
replyText.trim()
|
||||
replyText.trim() && !sending
|
||||
? "bg-primary text-primary-foreground hover:bg-primary/80"
|
||||
: "bg-muted text-muted-foreground cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<SendIcon className="w-4 h-4" />
|
||||
{sending ? (
|
||||
<svg className="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
) : (
|
||||
<SendIcon className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sendError && (
|
||||
<p className="text-xs text-destructive mt-2">{sendError}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user