admin/app/(dashboard)/support-tickets/[id]/page.tsx
alisaza 96603d31f7 feat(alert-modal): integrate alert modal for confirmation actions
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.
2026-09-13 10:14:18 +03:30

209 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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