admin/app/(dashboard)/support-tickets/[id]/page.tsx
alisaza 3b86c2c72f feat(api): add notifications endpoints for user notifications
Introduced new API endpoints for managing in-app notifications. The `/api/v1/notifications/me` endpoint retrieves a paginated list of notifications for the authenticated user, while the `/api/v1/notifications/me/unread-count` endpoint returns the count of unread notifications. Enhanced the OpenAPI documentation to reflect these changes, including detailed parameter descriptions and response schemas for better clarity and usability.
2026-09-13 13:04:02 +03:30

209 lines
7.7 KiB
TypeScript
Raw Permalink 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] ?? 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