admin/app/(dashboard)/support-tickets/[id]/page.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

149 lines
5.6 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 { 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 [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' })
}
}
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) => void changeStatus(event.target.value)}
>
{Object.entries(SUPPORT_ADMIN_STATUS_LABELS).map(([value, label]) => (
<option
key={value}
value={value}
>
{label}
</option>
))}
</select>
</label>
</div>
</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={() => void send()}
>
ثبت پاسخ و ارسال پیامک
</Button>
</div>
) : null}
</>
)}
</div>
</section>
)
}
export default AdminSupportTicketDetail