Files
clinicpro/assets/admin/pages/DashboardPage.test.tsx
T
hamed 773f9d4d16 feat: update session payment logic to ensure accurate payable amounts and reflect consumables in cost breakdown
- Adjusted the calculation of payable amounts in PaymentStep to align with server logic, ensuring overpayments are handled correctly.
- Enhanced DetailsStep to include consumables in the itemized cost breakdown, ensuring consistency with patient share calculations.
- Updated tests for SessionPaymentPage to validate new behavior regarding overpayments and consumable listings.
- Modified PatientController to register SessionPayment correctly when settling sessions via wallet, preventing double charges.
- Refactored WalletService to remove outdated methods and ensure wallet transactions reflect the correct amounts after discounts.
- Improved accessibility in SearchableSelect component by adding aria labels and ensuring proper role attributes for screen readers.
- Updated styles to ensure minimum touch targets meet WCAG guidelines for mobile usability.
2026-07-19 13:29:46 +03:30

160 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { screen } from '@testing-library/react';
import { renderWithProviders } from '../test/utils';
vi.mock('../lib/api', () => ({
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
ApiError: class extends Error {},
}));
import { api } from '../lib/api';
import { useAuthStore } from '../stores/authStore';
import DashboardPage from './DashboardPage';
import { JALALI_MONTHS } from '../components/dashboard/TauriDashboardView';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jalaali = require('jalaali-js') as { toJalaali: (d: Date) => { jy: number; jm: number } };
const get = api.get as ReturnType<typeof vi.fn>;
const clinicPayload = {
success: true,
data: {
clinic: { uuid: 'clinic-1', name: 'کلینیک نمونه', is_active: true, logo: null },
stats: {
total_doctors: 2,
today_appointments: 15,
this_month_appointments: 40,
pending_invitations: 0,
total_patients: 151,
revenue_period_rials: 5_600_002_000,
today_payments_rials: 5_225_000,
week_payments_rials: 12_000_000,
},
charts: {
revenue_by_day: [
{ label: '۷ خرداد', amount_rials: 60_000 },
{ label: '۸ خرداد', amount_rials: 160_000 },
{ label: '۹ خرداد', amount_rials: 90_000 },
],
revenue_by_month: [
{ label: 'فروردین', amount_rials: 60_000 },
{ label: 'اردیبهشت', amount_rials: 160_000 },
{ label: 'خرداد', amount_rials: 90_000 },
],
appointments_by_day: [
{ label: '۱', count: 15 },
{ label: '۲', count: 40 },
{ label: '۳', count: 48 },
],
},
charts_period: { patients_year: 1405, patients_month: 4, revenue_year: 1405 },
today_appointments: [
{
uuid: 'appt-1',
patient_name: 'دنیا خلیلی',
patient_mobile: '09136549874',
doctor_name: 'حمیدی',
service_name: 'ویزیت عمومی',
slot_start: 1_718_000_000,
slot_end: 1_718_001_800,
status: 'completed',
},
],
doctors: [],
period: { from: 0, to: 0 },
},
};
describe('DashboardPage (ported clinic dashboard)', () => {
beforeEach(() => {
get.mockReset();
useAuthStore.setState({ primaryRole: 'clinic', dbUuid: 'clinic-1', context: null } as never);
get.mockImplementation((url: string) => {
if (url.startsWith('/api/v1/dashboard/clinic')) return Promise.resolve(clinicPayload);
return Promise.resolve({ success: true, data: [] });
});
});
it('renders the ported stat cards, charts and new-appointments list', async () => {
renderWithProviders(<DashboardPage />, { route: '/admin/dashboard' });
// stat card labels (ported 1:1)
expect(await screen.findByText('تعداد کل مراجعین')).toBeInTheDocument();
expect(screen.getByText('کل پرداختی‌ها')).toBeInTheDocument();
expect(screen.getByText('پرداختی‌های امروز')).toBeInTheDocument();
expect(screen.getByText('تعداد نوبت‌های امروز')).toBeInTheDocument();
// chart titles
expect(screen.getByText('نمودار تعداد بیماران')).toBeInTheDocument();
expect(screen.getByText('میزان درآمد')).toBeInTheDocument();
// new-appointments list + row data (mobile/service columns come from the extended API)
expect(screen.getByText('لیست نوبت‌های جدید')).toBeInTheDocument();
expect(screen.getByText('دنیا خلیلی')).toBeInTheDocument();
expect(screen.getByText('09136549874')).toBeInTheDocument();
expect(screen.getByText('ویزیت عمومی')).toBeInTheDocument();
// ستون «وضعیت» هست، ولی در داشبورد فقط خواندنی (بدون queryKey → StatusPill نه dropdown)
expect(screen.getByText('وضعیت')).toBeInTheDocument();
expect(screen.getByText('ویزیت شده').closest('button')).toBeNull();
// کارت «پزشکان کلینیک» از داشبورد حذف شده
expect(screen.queryByText('پزشکان کلینیک')).not.toBeInTheDocument();
});
it('«مشاهده» به همان روزِ نوبت در لیست نوبت‌ها لینک می‌دهد', async () => {
renderWithProviders(<DashboardPage />, { 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(<DashboardPage />, { 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 () => {
renderWithProviders(<DashboardPage />, { route: '/admin/dashboard' });
await screen.findByText('تعداد کل مراجعین');
expect(get).toHaveBeenCalledWith(expect.stringContaining('/api/v1/dashboard/clinic'));
});
it('نمودارها را روی ماه و سال شمسی جاری درخواست می‌کند', async () => {
renderWithProviders(<DashboardPage />, { route: '/admin/dashboard' });
await screen.findByText('تعداد کل مراجعین');
const url = get.mock.calls.map(c => String(c[0])).find(u => u.startsWith('/api/v1/dashboard/clinic'))!;
const params = new URLSearchParams(url.split('?')[1]);
const now = jalaali.toJalaali(new Date());
expect(params.get('patients_year')).toBe(String(now.jy));
expect(params.get('patients_month')).toBe(String(now.jm));
expect(params.get('revenue_year')).toBe(String(now.jy));
// سلکتورها همان دوره را نشان می‌دهند
expect(screen.getByText(JALALI_MONTHS[now.jm - 1])).toBeInTheDocument();
expect(
screen.getByText(`سال ${new Intl.NumberFormat('fa-IR', { useGrouping: false }).format(now.jy)}`)
).toBeInTheDocument();
});
});