- Removed the "دکتر" prefix from doctor names in various components and API responses to ensure consistency and clarity. - Updated the AppointmentDetailPage, CommentsPage, DashboardPage, RatingsPage, SecretariesPage, and other relevant files to reflect the changes in doctor name formatting. - Adjusted API documentation to align with the new naming conventions. - Implemented validation to prevent the creation of clinics without a name and restricted users to a single clinic. - Added tests to verify that doctor names are stored without titles and that clinic creation adheres to the new validation rules.
169 lines
6.1 KiB
TypeScript
169 lines
6.1 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { CheckIcon, TrashIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
|
import { toast } from 'sonner';
|
|
import { api } from '../lib/api';
|
|
import type { ApiResponse, PaginatedResponse } from '../lib/api';
|
|
import type { Comment } from '../types';
|
|
import { formatDate } from '../lib/utils';
|
|
import DataTable, { Column } from '../components/ui/DataTable';
|
|
import { ActiveBadge } from '../components/ui/StatusBadge';
|
|
import Pagination from '../components/ui/Pagination';
|
|
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
|
|
|
const FILTERS = [
|
|
{ value: '', label: 'همه' },
|
|
{ value: 'pending', label: 'در انتظار' },
|
|
{ value: 'approved', label: 'تأییدشده' },
|
|
];
|
|
|
|
export default function CommentsPage() {
|
|
const qc = useQueryClient();
|
|
const [page, setPage] = useState(1);
|
|
const [search, setSearch] = useState('');
|
|
const [approvedFilter, setApprovedFilter] = useState('pending');
|
|
const [approveTarget, setApproveTarget] = useState<Comment | null>(null);
|
|
const [deleteTarget, setDeleteTarget] = useState<Comment | null>(null);
|
|
const limit = 15;
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['comments', page, search, approvedFilter],
|
|
queryFn: () => {
|
|
const params = new URLSearchParams({ page: String(page), limit: String(limit) });
|
|
if (search) params.set('search', search);
|
|
if (approvedFilter !== '') params.set('status', approvedFilter);
|
|
return api.get<PaginatedResponse<Comment>>(`/api/v1/admin/comments?${params}`);
|
|
},
|
|
});
|
|
|
|
const approveMutation = useMutation({
|
|
mutationFn: (c: Comment) => api.post<ApiResponse<null>>(`/api/v1/admin/comment/${c.uuid}/approve`, {}),
|
|
onSuccess: () => {
|
|
toast.success('نظر تأیید شد');
|
|
setApproveTarget(null);
|
|
qc.invalidateQueries({ queryKey: ['comments'] });
|
|
},
|
|
onError: (err: Error) => toast.error(err.message),
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (c: Comment) => api.delete<ApiResponse<null>>(`/api/v1/comment/${c.uuid}`),
|
|
onSuccess: () => {
|
|
toast.success('نظر حذف شد');
|
|
setDeleteTarget(null);
|
|
qc.invalidateQueries({ queryKey: ['comments'] });
|
|
},
|
|
onError: (err: Error) => toast.error(err.message),
|
|
});
|
|
|
|
const columns: Column<Comment>[] = [
|
|
{
|
|
key: 'patient_name',
|
|
header: 'کاربر',
|
|
render: (c) => <b>{c.patient_name}</b>,
|
|
},
|
|
{ key: 'doctor_name', header: 'پزشک', render: (c) => c.doctor_name },
|
|
{ key: 'title', header: 'عنوان' },
|
|
{
|
|
key: 'body',
|
|
header: 'متن',
|
|
render: (c) => (
|
|
<span className="muted" style={{ fontSize: 12, display: 'block', maxWidth: 280, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
{c.body}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'is_approved',
|
|
header: 'وضعیت',
|
|
render: (c) => <ActiveBadge active={c.is_approved} />,
|
|
},
|
|
{ key: 'created_at', header: 'تاریخ', render: (c) => formatDate(c.created_at) },
|
|
];
|
|
|
|
const items = data?.data ?? [];
|
|
const total = data?.meta?.totalRecords ?? 0;
|
|
|
|
return (
|
|
<div className="fade-in">
|
|
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
|
<div>
|
|
<h1 className="section-title">نظرات</h1>
|
|
<div className="muted">{total} نظر ثبتشده</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card">
|
|
<div className="card-pad" style={{ paddingBottom: 0 }}>
|
|
<div className="toolbar">
|
|
<div className="field" style={{ minWidth: 240 }}>
|
|
<MagnifyingGlassIcon style={{ width: 17, height: 17 }} />
|
|
<input
|
|
value={search}
|
|
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
|
placeholder="جستجو بر اساس نام پزشک یا متن..."
|
|
/>
|
|
</div>
|
|
<div className="seg">
|
|
{FILTERS.map((f) => (
|
|
<button
|
|
key={f.value}
|
|
className={approvedFilter === f.value ? 'on' : ''}
|
|
onClick={() => { setApprovedFilter(f.value); setPage(1); }}
|
|
>
|
|
{f.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<DataTable<Comment>
|
|
columns={columns}
|
|
data={items}
|
|
loading={isLoading}
|
|
emptyMessage="هیچ نظری یافت نشد"
|
|
actions={(comment) => (
|
|
<>
|
|
{!comment.is_approved && (
|
|
<button
|
|
onClick={() => setApproveTarget(comment)}
|
|
className="mini-btn"
|
|
title="تأیید"
|
|
>
|
|
<CheckIcon style={{ width: 15, height: 15 }} />
|
|
</button>
|
|
)}
|
|
<button onClick={() => setDeleteTarget(comment)}
|
|
className="mini-btn danger" title="حذف">
|
|
<TrashIcon style={{ width: 15, height: 15 }} />
|
|
</button>
|
|
</>
|
|
)}
|
|
/>
|
|
<Pagination page={page} total={total} limit={limit} onPageChange={setPage} />
|
|
</div>
|
|
|
|
<ConfirmDialog
|
|
open={!!approveTarget}
|
|
title="تأیید نظر"
|
|
message={`نظر "${approveTarget?.title}" از ${approveTarget?.patient_name} را تأیید میکنید؟`}
|
|
confirmLabel="تأیید"
|
|
loading={approveMutation.isPending}
|
|
onConfirm={() => approveTarget && approveMutation.mutate(approveTarget)}
|
|
onCancel={() => setApproveTarget(null)}
|
|
/>
|
|
|
|
<ConfirmDialog
|
|
open={!!deleteTarget}
|
|
title="حذف نظر"
|
|
message={`آیا از حذف نظر "${deleteTarget?.title}" از ${deleteTarget?.patient_name} اطمینان دارید؟`}
|
|
confirmLabel="حذف"
|
|
danger
|
|
loading={deleteMutation.isPending}
|
|
onConfirm={() => deleteTarget && deleteMutation.mutate(deleteTarget)}
|
|
onCancel={() => setDeleteTarget(null)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|