diff --git a/assets/admin/components/dashboard/NewAppointmentsTable.tsx b/assets/admin/components/dashboard/NewAppointmentsTable.tsx index 4451e170..30284185 100644 --- a/assets/admin/components/dashboard/NewAppointmentsTable.tsx +++ b/assets/admin/components/dashboard/NewAppointmentsTable.tsx @@ -20,6 +20,16 @@ export interface ApptRow { status: string; } +/** + * روزِ محلیِ نوبت (YYYY-MM-DD) برای دیپ‌لینک «مشاهده» به همان تاریخ در لیست نوبت‌ها. + * همان قراردادی که AppointmentDetailPage/AppointmentsPage استفاده می‌کنند. + */ +const isoDay = (ts?: number | null): string => { + if (!ts) return ''; + const d = new Date(ts * 1000); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; +}; + function formatTime(ts?: number | null): string { if (!ts) return '—'; return new Intl.DateTimeFormat('fa-IR', { timeZone: 'Asia/Tehran', hour: '2-digit', minute: '2-digit' }).format(new Date(ts * 1000)); @@ -46,7 +56,7 @@ export function NewAppointmentsTable({ rows, loading }: { rows: ApptRow[]; loadi {HEAD.map((h, i) => ( {h} @@ -59,16 +69,16 @@ export function NewAppointmentsTable({ rows, loading }: { rows: ApptRow[]; loadi key={r.uuid} className="border-b border-[#DBDBDB] dark:border-transparent hover:bg-[#f4f5fd] dark:hover:bg-[#2a2c3d] transition-colors" > - {new Intl.NumberFormat('fa-IR').format(idx + 1)} - {r.patient_name || '—'} - {r.patient_mobile || '—'} - {formatTime(r.slot_start)} - {formatTime(r.slot_end)} - {r.service_name || '—'} - {r.doctor_name ? `دکتر ${r.doctor_name}` : '—'} + {new Intl.NumberFormat('fa-IR').format(idx + 1)} + {r.patient_name || '—'} + {r.patient_mobile ? {r.patient_mobile} : '—'} + {formatTime(r.slot_start)} + {formatTime(r.slot_end)} + {r.service_name || '—'} + {r.doctor_name ? `دکتر ${r.doctor_name}` : '—'} مشاهده @@ -82,11 +92,11 @@ export function NewAppointmentsTable({ rows, loading }: { rows: ApptRow[]; loadi ); } -function Cell({ children, className = '', dir }: { children: React.ReactNode; className?: string; dir?: 'ltr' | 'rtl' }) { +/** همه‌ی سلول‌ها text-start هستند تا دقیقاً زیر هدر هم‌نامشان بنشینند. */ +function Cell({ children, className = '' }: { children: React.ReactNode; className?: string }) { return ( {children} diff --git a/assets/admin/components/dashboard/TauriCharts.test.tsx b/assets/admin/components/dashboard/TauriCharts.test.tsx index 18f5b4ba..3d1f856a 100644 --- a/assets/admin/components/dashboard/TauriCharts.test.tsx +++ b/assets/admin/components/dashboard/TauriCharts.test.tsx @@ -27,6 +27,24 @@ describe('TauriBarChart', () => { expect(screen.queryByText(EMPTY)).not.toBeInTheDocument(); expect(screen.getByText('۸ خرداد')).toBeInTheDocument(); }); + + it('renders one bar per day of a full Jalali month', () => { + const month: ChartPoint[] = Array.from({ length: 31 }, (_, i) => ({ label: String(i + 1), value: i % 5 })); + const { container } = render(); + expect(container.querySelectorAll('.bg-\\[\\#5559CE\\]')).toHaveLength(31); + }); + + /** + * styles.css ships an unlayered legacy `.flex { align-items: center }` that + * beats Tailwind's `items-*` utilities and collapses the plot column to zero + * height. The frame must pin alignment inline, or the chart renders blank. + */ + it('pins flex alignment inline so the legacy global .flex cannot collapse it', () => { + const { container } = render(); + const frame = container.querySelector('.h-\\[300px\\]') as HTMLElement; + expect(frame.style.alignItems).toBe('stretch'); + expect((frame.firstElementChild as HTMLElement).style.alignItems).toBe('stretch'); + }); }); describe('TauriLineChart', () => { diff --git a/assets/admin/components/dashboard/TauriCharts.tsx b/assets/admin/components/dashboard/TauriCharts.tsx index 9f36f252..f89243f6 100644 --- a/assets/admin/components/dashboard/TauriCharts.tsx +++ b/assets/admin/components/dashboard/TauriCharts.tsx @@ -47,13 +47,16 @@ function ChartFrame({ yWidth: number; children: React.ReactNode; }) { + // NOTE: styles.css ships an unlayered legacy `.flex { align-items: center }` + // which beats Tailwind's layered `items-*` utilities, collapsing the plot + // column to zero height. Inline `alignItems` is the only reliable override. return ( -
-
+
+
{/* y-axis ticks, aligned to gridlines */}
{ticks.map((t, i) => ( @@ -74,7 +77,7 @@ function ChartFrame({
{/* x-axis labels */} -
+
{labels.map((l, i) => ( {l} @@ -95,9 +98,9 @@ export function TauriBarChart({ data }: { data: ChartPoint[] }) { const top = ticks[0] || 1; return ( d.label)} yWidth={42}> -
+
{data.map((d, i) => ( -
+
{ // ستون «وضعیت» از داشبورد حذف شده — تغییر وضعیت فقط در صفحه نوبت‌هاست expect(screen.queryByText('وضعیت')).not.toBeInTheDocument(); expect(screen.queryByText('ویزیت شده')).not.toBeInTheDocument(); + + // کارت «پزشکان کلینیک» از داشبورد حذف شده + expect(screen.queryByText('پزشکان کلینیک')).not.toBeInTheDocument(); + }); + + it('«مشاهده» به همان روزِ نوبت در لیست نوبت‌ها لینک می‌دهد', async () => { + renderWithProviders(, { route: '/admin/dashboard' }); + await screen.findByText('دنیا خلیلی'); + + const d = new Date(1_718_000_000 * 1000); + const day = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + + expect(screen.getByText('مشاهده').closest('a')).toHaveAttribute('href', `/admin/appointments?date=${day}`); + }); + + it('سلول‌های جدول با هدرهایشان هم‌تراز هستند (همه text-start جز ستون عملیات)', async () => { + const { container } = renderWithProviders(, { route: '/admin/dashboard' }); + await screen.findByText('دنیا خلیلی'); + + const heads = Array.from(container.querySelectorAll('thead th')); + const cells = Array.from(container.querySelectorAll('tbody td')); + expect(heads).toHaveLength(cells.length); + + heads.slice(0, -1).forEach(th => expect(th.className).toContain('text-start')); + cells.slice(0, -1).forEach(td => expect(td.className).toContain('text-start')); + + // ستون آخر «عملیات» در هر دو سطر وسط‌چین است + expect(heads[heads.length - 1].className).toContain('text-center'); + expect(cells[cells.length - 1].className).toContain('text-center'); + + // dir=ltr روی سلول نمی‌نشیند (جهت را برمی‌گرداند و هم‌ترازی را می‌شکند) + cells.forEach(td => expect(td.getAttribute('dir')).toBeNull()); }); it('calls the real clinic dashboard endpoint', async () => { diff --git a/assets/admin/pages/DashboardPage.tsx b/assets/admin/pages/DashboardPage.tsx index f3323048..5ac8edc9 100644 --- a/assets/admin/pages/DashboardPage.tsx +++ b/assets/admin/pages/DashboardPage.tsx @@ -11,7 +11,6 @@ import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; import { formatNumber, formatRial, formatDateTime } from '../lib/utils'; import { useAuthStore } from '../stores/authStore'; -import InviteDoctorModal from '../components/ui/InviteDoctorModal'; import { TauriDashboardView } from '../components/dashboard/TauriDashboardView'; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -638,8 +637,6 @@ interface ClinicDashboardData { } function ClinicDashboard() { - const { context, dbUuid } = useAuthStore(); - const [inviteOpen, setInviteOpen] = useState(false); const preset: DatePreset = 'this_month'; const range = getRange(preset); const chartPeriod = useJalaliChartPeriod(); @@ -651,7 +648,6 @@ function ClinicDashboard() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const d = useMemo(() => (q.data?.data as any)?.data ?? q.data?.data, [q.data]); - const clinicUuid = d?.clinic.uuid ?? dbUuid ?? ''; if (q.isLoading) return ; @@ -677,57 +673,6 @@ function ClinicDashboard() { currentJalaliYear={chartPeriod.currentJalaliYear} /> - {/* پزشکان کلینیک — بخش عملکردی موجود clinicpro، حفظ‌شده زیر نمای منتقل‌شده */} -
-
-
-

پزشکان کلینیک

-
- - - {clinicUuid && ( - مدیریت - )} -
-
- {!d?.doctors.length ? ( -
-

هنوز پزشکی دعوت نشده

- -
- ) : ( -
- {d.doctors.map((doc, i) => ( -
- -
- دکتر {doc.name} -
- {formatNumber(doc.today_count)} امروز -
- ))} -
- )} -
-
- - {inviteOpen && clinicUuid && ( - setInviteOpen(false)} - onInvited={() => q.refetch()} - /> - )}
); }