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:
parent
4c5f4d5983
commit
9254a8b487
@ -7,6 +7,7 @@ import PageNavbar from '@/components/layouts/PageNavbar'
|
|||||||
import Button from '@/components/formElements/Button'
|
import Button from '@/components/formElements/Button'
|
||||||
import Input from '@/components/formElements/Input'
|
import Input from '@/components/formElements/Input'
|
||||||
import { APP_ROUTES } from '@/constants/routes'
|
import { APP_ROUTES } from '@/constants/routes'
|
||||||
|
import useAlertModal from '@/hooks/useAlertModal'
|
||||||
import { addToast } from '@/lib/toast'
|
import { addToast } from '@/lib/toast'
|
||||||
import { formatIranianMobile, formatPersianDate } from '@/lib/formatters'
|
import { formatIranianMobile, formatPersianDate } from '@/lib/formatters'
|
||||||
import {
|
import {
|
||||||
@ -21,6 +22,7 @@ import {
|
|||||||
|
|
||||||
const AdminSupportTicketDetail = () => {
|
const AdminSupportTicketDetail = () => {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
|
const { showAlert } = useAlertModal()
|
||||||
const [ticket, setTicket] = useState<SupportTicket | null>(null)
|
const [ticket, setTicket] = useState<SupportTicket | null>(null)
|
||||||
const [reply, setReply] = useState('')
|
const [reply, setReply] = useState('')
|
||||||
const [pending, setPending] = useState(false)
|
const [pending, setPending] = useState(false)
|
||||||
@ -56,6 +58,17 @@ const AdminSupportTicketDetail = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const confirmClose = () => {
|
||||||
|
showAlert(
|
||||||
|
'این تیکت بسته شود؟ کاربر دیگر نمیتواند پیام بفرستد و در صورت بستن توسط ادمین، خودش نمیتواند دوباره باز کند.',
|
||||||
|
() => {
|
||||||
|
void changeStatus('closed')
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ dangerAccept: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="h-full w-full text-right">
|
<section className="h-full w-full text-right">
|
||||||
<PageNavbar pageTitle={ticket?.subject ?? 'جزئیات تیکت'} />
|
<PageNavbar pageTitle={ticket?.subject ?? 'جزئیات تیکت'} />
|
||||||
@ -104,6 +117,24 @@ const AdminSupportTicketDetail = () => {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
||||||
<div className="space-y-3 rounded-2xl border border-default-200 bg-white p-5">
|
<div className="space-y-3 rounded-2xl border border-default-200 bg-white p-5">
|
||||||
{ticket.messages.map((message) => (
|
{ticket.messages.map((message) => (
|
||||||
@ -129,7 +160,10 @@ const AdminSupportTicketDetail = () => {
|
|||||||
setReply(String(value))
|
setReply(String(value))
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="my-3 text-xs text-text-muted">پس از ثبت پاسخ، برای کاربر پیامک اطلاعرسانی ارسال میشود.</p>
|
<p className="my-3 text-xs text-text-muted">
|
||||||
|
میتوانید چند پیام پشتسرهم بفرستید. کاربر فقط بعد از پاسخ شما یک پیام میتواند بفرستد. پس از ثبت پاسخ، برای کاربر پیامک
|
||||||
|
اطلاعرسانی ارسال میشود.
|
||||||
|
</p>
|
||||||
<Button
|
<Button
|
||||||
isLoading={pending}
|
isLoading={pending}
|
||||||
onClick={() => void send()}
|
onClick={() => void send()}
|
||||||
|
|||||||
48
openapi.json
48
openapi.json
@ -16112,6 +16112,7 @@
|
|||||||
},
|
},
|
||||||
"/api/v1/support-tickets/{id}/replies": {
|
"/api/v1/support-tickets/{id}/replies": {
|
||||||
"post": {
|
"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",
|
"operationId": "SupportTicketsController_reply",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@ -16154,6 +16155,41 @@
|
|||||||
"tags": ["Support Tickets"]
|
"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": {
|
"/api/v1/admin/support-tickets": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "AdminSupportTicketsController_list",
|
"operationId": "AdminSupportTicketsController_list",
|
||||||
@ -28493,6 +28529,12 @@
|
|||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
|
"closedBy": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["user", "admin"],
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Who closed the ticket. Consumers cannot reopen admin-closed tickets."
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
@ -28544,6 +28586,12 @@
|
|||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
|
"closedBy": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["user", "admin"],
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Who closed the ticket. Consumers cannot reopen admin-closed tickets."
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export interface SupportTicket {
|
|||||||
status: string
|
status: string
|
||||||
lastMessageAt: string
|
lastMessageAt: string
|
||||||
closedAt: string | null
|
closedAt: string | null
|
||||||
|
closedBy?: 'user' | 'admin' | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
user: { mobile: string; displayName: string | null }
|
user: { mobile: string; displayName: string | null }
|
||||||
messages: SupportTicketMessage[]
|
messages: SupportTicketMessage[]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user