From 5af360fd15d66a36a807a07b39c6ef9b40d049f1 Mon Sep 17 00:00:00 2001 From: Oleg Date: Wed, 19 Aug 2026 07:54:23 +0000 Subject: [PATCH] Support attaching a file / pasting a screenshot when creating a new ticket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "New ticket" modal only took a plain-text message. It now matches the existing reply box on an open ticket: a "Прикрепить файл" button, and Ctrl+V paste of a clipboard screenshot attaches it directly. POST /api/tickets switched from JSON to multipart/form-data to carry the optional file, reusing the same saveAttachment/attachment-delivery path replies already use. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01GteWhnWKTmnXcsd5jx6H7u --- .../(admin)/dashboard/new-ticket-modal.tsx | 67 +++++++++++++++---- src/app/api/tickets/route.ts | 46 +++++++++++-- src/lib/tickets/service.ts | 2 + 3 files changed, 98 insertions(+), 17 deletions(-) diff --git a/src/app/(admin)/dashboard/new-ticket-modal.tsx b/src/app/(admin)/dashboard/new-ticket-modal.tsx index 044325c..275371f 100644 --- a/src/app/(admin)/dashboard/new-ticket-modal.tsx +++ b/src/app/(admin)/dashboard/new-ticket-modal.tsx @@ -1,23 +1,27 @@ "use client"; -import { useState } from "react"; +import { useRef, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; -import { X } from "lucide-react"; +import { Paperclip, X } from "lucide-react"; export function NewTicketModal({ open, onClose }: { open: boolean; onClose: () => void }) { const [subject, setSubject] = useState(""); const [customerName, setCustomerName] = useState(""); const [customerEmail, setCustomerEmail] = useState(""); const [body, setBody] = useState(""); + const [pendingFile, setPendingFile] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const fileInputRef = useRef(null); function reset() { setSubject(""); setCustomerName(""); setCustomerEmail(""); setBody(""); + setPendingFile(null); setError(null); + if (fileInputRef.current) fileInputRef.current.value = ""; } async function handleSubmit(e: React.FormEvent) { @@ -25,16 +29,14 @@ export function NewTicketModal({ open, onClose }: { open: boolean; onClose: () = setLoading(true); setError(null); - const res = await fetch("/api/tickets", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - subject, - customerName, - customerEmail: customerEmail || undefined, - body, - }), - }); + const formData = new FormData(); + formData.set("subject", subject); + formData.set("customerName", customerName); + if (customerEmail) formData.set("customerEmail", customerEmail); + formData.set("body", body); + if (pendingFile) formData.set("file", pendingFile); + + const res = await fetch("/api/tickets", { method: "POST", body: formData }); setLoading(false); if (!res.ok) { @@ -104,17 +106,56 @@ export function NewTicketModal({ open, onClose }: { open: boolean; onClose: () = -