Enhanced various components to utilize the alert modal for user confirmations before executing critical actions. This includes marking messages as read, saving notification rules, restoring reviews, sending replies, changing ticket statuses, and managing event commissions. The integration improves user experience by ensuring actions are intentional and provides clear feedback on the outcomes.
209 lines
7.7 KiB
TypeScript
209 lines
7.7 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect, useState } from 'react'
|
||
import { useParams } from 'next/navigation'
|
||
|
||
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 {
|
||
adminReplyToSupportTicket,
|
||
getAdminSupportTicket,
|
||
SUPPORT_ADMIN_STATUS_LABELS,
|
||
SUPPORT_CATEGORY_LABELS,
|
||
SUPPORT_PRIORITY_LABELS,
|
||
updateSupportTicketStatus,
|
||
type SupportTicket,
|
||
} from '@/services/supportTickets'
|
||
|
||
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)
|
||
|
||
useEffect(() => {
|
||
void getAdminSupportTicket(id)
|
||
.then(setTicket)
|
||
.catch(() => addToast({ title: 'دریافت تیکت ناموفق بود', color: 'danger' }))
|
||
}, [id])
|
||
|
||
const send = async () => {
|
||
if (!reply.trim()) return
|
||
try {
|
||
setPending(true)
|
||
const updated = await adminReplyToSupportTicket(id, reply)
|
||
|
||
setTicket(updated)
|
||
setReply('')
|
||
addToast({ title: 'پاسخ ثبت شد و پیامک کاربر در صف ارسال قرار گرفت', color: 'success' })
|
||
} catch {
|
||
addToast({ title: 'ثبت پاسخ ناموفق بود', color: 'danger' })
|
||
} finally {
|
||
setPending(false)
|
||
}
|
||
}
|
||
|
||
const changeStatus = async (status: string) => {
|
||
try {
|
||
setTicket(await updateSupportTicketStatus(id, status))
|
||
addToast({ title: 'وضعیت تیکت تغییر کرد', color: 'success' })
|
||
} catch {
|
||
addToast({ title: 'تغییر وضعیت ناموفق بود', color: 'danger' })
|
||
}
|
||
}
|
||
|
||
const confirmSend = () => {
|
||
if (!reply.trim()) return
|
||
|
||
showAlert('این پاسخ ثبت و پیامک اطلاعرسانی برای کاربر ارسال شود؟', () => {
|
||
void send()
|
||
})
|
||
}
|
||
|
||
const confirmStatusChange = (nextStatus: string) => {
|
||
if (!ticket || nextStatus === ticket.status) return
|
||
|
||
if (nextStatus === 'closed') {
|
||
showAlert(
|
||
'این تیکت بسته شود؟ کاربر دیگر نمیتواند پیام بفرستد و در صورت بستن توسط ادمین، خودش نمیتواند دوباره باز کند.',
|
||
() => {
|
||
void changeStatus('closed')
|
||
},
|
||
undefined,
|
||
{ dangerAccept: true }
|
||
)
|
||
|
||
return
|
||
}
|
||
|
||
const label = SUPPORT_ADMIN_STATUS_LABELS[nextStatus as keyof typeof SUPPORT_ADMIN_STATUS_LABELS] ?? nextStatus
|
||
|
||
showAlert(`وضعیت تیکت به «${label}» تغییر کند؟`, () => {
|
||
void changeStatus(nextStatus)
|
||
})
|
||
}
|
||
|
||
const confirmClose = () => {
|
||
confirmStatusChange('closed')
|
||
}
|
||
|
||
return (
|
||
<section className="h-full w-full text-right">
|
||
<PageNavbar pageTitle={ticket?.subject ?? 'جزئیات تیکت'} />
|
||
<div className="admin-page-container mx-auto max-w-4xl space-y-5">
|
||
<Button
|
||
size="sm"
|
||
to={APP_ROUTES.SUPPORT_TICKETS}
|
||
variant="light"
|
||
>
|
||
بازگشت به تیکتها
|
||
</Button>
|
||
{!ticket ? (
|
||
<p className="py-12 text-center text-text-muted">در حال دریافت…</p>
|
||
) : (
|
||
<>
|
||
<div className="rounded-2xl border border-default-200 bg-white p-5">
|
||
<div className="grid gap-3 text-sm sm:grid-cols-2 lg:grid-cols-4">
|
||
<div>
|
||
<p className="text-text-muted">کاربر</p>
|
||
<strong>{ticket.user.displayName || 'کاربر'}</strong>
|
||
<p dir="ltr">{formatIranianMobile(ticket.user.mobile)}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-text-muted">دستهبندی</p>
|
||
<strong>{SUPPORT_CATEGORY_LABELS[ticket.category]}</strong>
|
||
</div>
|
||
<div>
|
||
<p className="text-text-muted">اولویت</p>
|
||
<strong>{SUPPORT_PRIORITY_LABELS[ticket.priority]}</strong>
|
||
</div>
|
||
<label>
|
||
<span className="text-text-muted">وضعیت</span>
|
||
<select
|
||
className="mt-1 block w-full rounded-lg border border-default-300 p-2"
|
||
value={ticket.status}
|
||
onChange={(event) => {
|
||
confirmStatusChange(event.target.value)
|
||
}}
|
||
>
|
||
{Object.entries(SUPPORT_ADMIN_STATUS_LABELS).map(([value, label]) => (
|
||
<option
|
||
key={value}
|
||
value={value}
|
||
>
|
||
{label}
|
||
</option>
|
||
))}
|
||
</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) => (
|
||
<div
|
||
key={message.id}
|
||
className={`max-w-[82%] rounded-2xl p-4 ${message.isAdmin ? 'mr-auto bg-primary text-white' : 'ml-auto bg-default-100'}`}
|
||
>
|
||
<p className="text-xs opacity-70">{message.isAdmin ? 'پشتیبانی' : ticket.user.displayName || 'کاربر'}</p>
|
||
<p className="mt-1 whitespace-pre-wrap leading-7">{message.body}</p>
|
||
<p className="mt-2 text-xs opacity-60">{formatPersianDate(message.createdAt)}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{ticket.status !== 'closed' ? (
|
||
<div className="rounded-2xl border border-default-200 bg-white p-5">
|
||
<Input
|
||
generalType="textarea"
|
||
label="پاسخ پشتیبانی"
|
||
name="adminReply"
|
||
textAreaMinRows={5}
|
||
value={reply}
|
||
onValueChange={(value) => {
|
||
setReply(String(value))
|
||
}}
|
||
/>
|
||
<p className="my-3 text-xs text-text-muted">
|
||
میتوانید چند پیام پشتسرهم بفرستید. کاربر فقط بعد از پاسخ شما یک پیام میتواند بفرستد. پس از ثبت پاسخ، برای کاربر پیامک
|
||
اطلاعرسانی ارسال میشود.
|
||
</p>
|
||
<Button
|
||
isLoading={pending}
|
||
onClick={confirmSend}
|
||
>
|
||
ثبت پاسخ و ارسال پیامک
|
||
</Button>
|
||
</div>
|
||
) : null}
|
||
</>
|
||
)}
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
export default AdminSupportTicketDetail
|