- 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.
124 lines
4.8 KiB
TypeScript
124 lines
4.8 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { TrashIcon, StarIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
|
import { StarIcon as StarSolid } from '@heroicons/react/24/solid';
|
|
import { toast } from 'sonner';
|
|
import { api } from '../lib/api';
|
|
import type { ApiResponse, PaginatedResponse } from '../lib/api';
|
|
import type { Rating } from '../types';
|
|
import { formatDate } from '../lib/utils';
|
|
import DataTable, { Column } from '../components/ui/DataTable';
|
|
import Pagination from '../components/ui/Pagination';
|
|
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
|
|
|
function Stars({ value }: { value: number }) {
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
{[1, 2, 3, 4, 5].map((i) =>
|
|
i <= value
|
|
? <StarSolid key={i} style={{ width: 14, height: 14, color: 'oklch(0.78 0.18 85)' }} />
|
|
: <StarIcon key={i} style={{ width: 14, height: 14, color: 'var(--border)' }} />
|
|
)}
|
|
<span className="muted" style={{ fontSize: 12, marginRight: 4 }}>{value}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function RatingsPage() {
|
|
const qc = useQueryClient();
|
|
const [page, setPage] = useState(1);
|
|
const [search, setSearch] = useState('');
|
|
const [deleteTarget, setDeleteTarget] = useState<Rating | null>(null);
|
|
const limit = 15;
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['ratings', page, search],
|
|
queryFn: () => {
|
|
const params = new URLSearchParams({ page: String(page), limit: String(limit) });
|
|
if (search) params.set('search', search);
|
|
return api.get<PaginatedResponse<Rating>>(`/api/v1/admin/rates?${params}`);
|
|
},
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (r: Rating) => api.delete<ApiResponse<null>>(`/api/v1/rate/${r.uuid}`),
|
|
onSuccess: () => {
|
|
toast.success('امتیاز حذف شد');
|
|
setDeleteTarget(null);
|
|
qc.invalidateQueries({ queryKey: ['ratings'] });
|
|
},
|
|
onError: (err: Error) => toast.error(err.message),
|
|
});
|
|
|
|
const columns: Column<Rating>[] = [
|
|
{ key: 'patient_name', header: 'بیمار', render: (r) => <b>{r.patient_name}</b> },
|
|
{ key: 'doctor_name', header: 'پزشک', render: (r) => r.doctor_name },
|
|
{ key: 'overall', header: 'کلی', render: (r) => <Stars value={r.overall} /> },
|
|
{
|
|
key: 'ratings_detail',
|
|
header: 'جزئیات',
|
|
render: (r) => (
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '2px 12px', fontSize: 12 }}>
|
|
<span className="muted">تشخیص:</span><Stars value={r.diagnosis_accuracy} />
|
|
<span className="muted">مهارت:</span><Stars value={r.skill} />
|
|
<span className="muted">رفتار:</span><Stars value={r.behavior} />
|
|
</div>
|
|
),
|
|
},
|
|
{ key: 'created_at', header: 'تاریخ', render: (r) => formatDate(r.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>
|
|
</div>
|
|
<DataTable<Rating>
|
|
columns={columns}
|
|
data={items}
|
|
loading={isLoading}
|
|
emptyMessage="هیچ امتیازی یافت نشد"
|
|
actions={(rating) => (
|
|
<button onClick={() => setDeleteTarget(rating)}
|
|
className="mini-btn danger" title="حذف">
|
|
<TrashIcon style={{ width: 15, height: 15 }} />
|
|
</button>
|
|
)}
|
|
/>
|
|
<Pagination page={page} total={total} limit={limit} onPageChange={setPage} />
|
|
</div>
|
|
|
|
<ConfirmDialog
|
|
open={!!deleteTarget}
|
|
title="حذف امتیاز"
|
|
message={`آیا از حذف امتیاز ${deleteTarget?.patient_name} برای ${deleteTarget?.doctor_name} اطمینان دارید؟`}
|
|
confirmLabel="حذف"
|
|
danger
|
|
loading={deleteMutation.isPending}
|
|
onConfirm={() => deleteTarget && deleteMutation.mutate(deleteTarget)}
|
|
onCancel={() => setDeleteTarget(null)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|