feat(support-tickets): add close action and turn-based reply guidance

Let admins close tickets with confirmation and surface closedBy so consumer reopen rules stay clear.
This commit is contained in:
alisaza 2026-09-11 21:31:16 +03:30
parent 4c5f4d5983
commit 9254a8b487
3 changed files with 84 additions and 1 deletions

View File

@ -7,6 +7,7 @@ import PageNavbar from '@/components/layouts/PageNavbar'
import Button from '@/components/formElements/Button'
import Input from '@/components/formElements/Input'
import { APP_ROUTES } from '@/constants/routes'
import useAlertModal from '@/hooks/useAlertModal'
import { addToast } from '@/lib/toast'
import { formatIranianMobile, formatPersianDate } from '@/lib/formatters'
import {
@ -21,6 +22,7 @@ import {
const AdminSupportTicketDetail = () => {
const { id } = useParams<{ id: string }>()
const { showAlert } = useAlertModal()
const [ticket, setTicket] = useState<SupportTicket | null>(null)
const [reply, setReply] = useState('')
const [pending, setPending] = useState(false)
@ -56,6 +58,17 @@ const AdminSupportTicketDetail = () => {
}
}
const confirmClose = () => {
showAlert(
'این تیکت بسته شود؟ کاربر دیگر نمی‌تواند پیام بفرستد و در صورت بستن توسط ادمین، خودش نمی‌تواند دوباره باز کند.',
() => {
void changeStatus('closed')
},
undefined,
{ dangerAccept: true }
)
}
return (
<section className="h-full w-full text-right">
<PageNavbar pageTitle={ticket?.subject ?? 'جزئیات تیکت'} />
@ -104,6 +117,24 @@ const AdminSupportTicketDetail = () => {
</select>
</label>
</div>
{ticket.status !== 'closed' ? (
<div className="mt-4 flex justify-end">
<Button
color="danger"
size="sm"
variant="flat"
onClick={confirmClose}
>
بستن تیکت
</Button>
</div>
) : (
<p className="mt-4 text-sm text-text-muted">
{ticket.closedBy === 'user'
? 'این تیکت توسط کاربر بسته شده است.'
: 'این تیکت توسط پشتیبانی بسته شده است؛ کاربر نمی‌تواند دوباره باز کند.'}
</p>
)}
</div>
<div className="space-y-3 rounded-2xl border border-default-200 bg-white p-5">
{ticket.messages.map((message) => (
@ -129,7 +160,10 @@ const AdminSupportTicketDetail = () => {
setReply(String(value))
}}
/>
<p className="my-3 text-xs text-text-muted">پس از ثبت پاسخ، برای کاربر پیامک اطلاعرسانی ارسال میشود.</p>
<p className="my-3 text-xs text-text-muted">
میتوانید چند پیام پشتسرهم بفرستید. کاربر فقط بعد از پاسخ شما یک پیام میتواند بفرستد. پس از ثبت پاسخ، برای کاربر پیامک
اطلاعرسانی ارسال میشود.
</p>
<Button
isLoading={pending}
onClick={() => void send()}

View File

@ -16112,6 +16112,7 @@
},
"/api/v1/support-tickets/{id}/replies": {
"post": {
"description": "Allowed only while status is waiting_user (after a support reply). One consumer message returns the ticket to waiting_admin.",
"operationId": "SupportTicketsController_reply",
"parameters": [
{
@ -16154,6 +16155,41 @@
"tags": ["Support Tickets"]
}
},
"/api/v1/support-tickets/{id}/close": {
"post": {
"description": "Consumers may close their own open tickets. Admin-closed tickets stay closed and cannot be reopened by the consumer.",
"operationId": "SupportTicketsController_close",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SupportTicketSummaryResponseDto"
}
}
}
}
},
"security": [
{
"JWT-auth": []
}
],
"summary": "Close my support ticket",
"tags": ["Support Tickets"]
}
},
"/api/v1/admin/support-tickets": {
"get": {
"operationId": "AdminSupportTicketsController_list",
@ -28493,6 +28529,12 @@
"format": "date-time",
"nullable": true
},
"closedBy": {
"type": "string",
"enum": ["user", "admin"],
"nullable": true,
"description": "Who closed the ticket. Consumers cannot reopen admin-closed tickets."
},
"createdAt": {
"type": "string",
"format": "date-time"
@ -28544,6 +28586,12 @@
"format": "date-time",
"nullable": true
},
"closedBy": {
"type": "string",
"enum": ["user", "admin"],
"nullable": true,
"description": "Who closed the ticket. Consumers cannot reopen admin-closed tickets."
},
"createdAt": {
"type": "string",
"format": "date-time"

View File

@ -19,6 +19,7 @@ export interface SupportTicket {
status: string
lastMessageAt: string
closedAt: string | null
closedBy?: 'user' | 'admin' | null
createdAt: string
user: { mobile: string; displayName: string | null }
messages: SupportTicketMessage[]