feat: enhance NewAppointmentsTable with dynamic patient record navigation and update DashboardPage tests
This commit is contained in:
@@ -8,9 +8,11 @@
|
||||
* editable in place only when the row carries a `version` (optimistic lock)
|
||||
* and the parent passes the query key to invalidate.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import AppointmentStatusDropdown, { STATUS_META } from '../ui/AppointmentStatusDropdown';
|
||||
import { findRecordUuid } from '../AppointmentActions';
|
||||
|
||||
export interface ApptRow {
|
||||
uuid: string;
|
||||
@@ -95,12 +97,7 @@ export function NewAppointmentsTable({ rows, loading, queryKey, emptyText }: Pro
|
||||
: <StatusPill status={r.status} />}
|
||||
</Cell>
|
||||
<td className="py-[10px] px-[18px] text-center">
|
||||
<Link
|
||||
to={isoDay(r.slot_start) ? `/admin/appointments?date=${isoDay(r.slot_start)}` : '/admin/appointments'}
|
||||
className="text-[var(--primary)] text-[12px] font-medium hover:underline whitespace-nowrap"
|
||||
>
|
||||
مشاهده
|
||||
</Link>
|
||||
<ViewLink row={r} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -110,6 +107,46 @@ export function NewAppointmentsTable({ rows, loading, queryKey, emptyText }: Pro
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* وضعیتهایی که نوبت از مرحلهٔ «قطعیشدن» رد شده — در اینها پروندهٔ بیمار حتماً
|
||||
* ساخته شده است (ساخت پرونده اثر جانبیِ قطعیکردن است).
|
||||
*/
|
||||
const CONFIRMED_STATUSES = ['confirmed', 'following_up', 'salon', 'completed', 'no_show'];
|
||||
|
||||
/**
|
||||
* «مشاهده» — نوبتِ قطعیشده به **پروندهٔ بیمار** میرود (چون از لحظهٔ قطعیشدن پرونده
|
||||
* دارد)، بقیه به لیست نوبتهای همان روز. uuid پرونده روی ردیف نیست، پس با موبایلِ
|
||||
* بیمار resolve میشود — همان مسیری که منوی عملیات نوبتها استفاده میکند.
|
||||
*/
|
||||
function ViewLink({ row }: { row: ApptRow }) {
|
||||
const navigate = useNavigate();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const dayHref = isoDay(row.slot_start) ? `/admin/appointments?date=${isoDay(row.slot_start)}` : '/admin/appointments';
|
||||
const cls = 'text-[var(--primary)] text-[12px] font-medium hover:underline whitespace-nowrap';
|
||||
|
||||
if (!CONFIRMED_STATUSES.includes(row.status) || !row.patient_mobile) {
|
||||
return <Link to={dayHref} className={cls}>مشاهده</Link>;
|
||||
}
|
||||
|
||||
const openRecord = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
const recordUuid = await findRecordUuid(row.patient_mobile!);
|
||||
navigate(recordUuid ? `/admin/patients/${recordUuid}` : dayHref);
|
||||
} catch {
|
||||
toast.error('خطا در یافتن پرونده بیمار');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button type="button" onClick={openRecord} disabled={busy} className={`${cls} bg-transparent border-none cursor-pointer`}>
|
||||
{busy ? '...' : 'مشاهده'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/** نمایش فقطخواندنی وضعیت — وقتی version یا queryKey در دسترس نیست. */
|
||||
function StatusPill({ status }: { status: string }) {
|
||||
const meta = STATUS_META[status] ?? { label: status, color: 'var(--text-3)' };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { renderWithProviders } from '../test/utils';
|
||||
|
||||
vi.mock('../lib/api', () => ({
|
||||
@@ -7,6 +7,12 @@ vi.mock('../lib/api', () => ({
|
||||
ApiError: class extends Error {},
|
||||
}));
|
||||
|
||||
const navigate = vi.fn();
|
||||
vi.mock('react-router-dom', async () => ({
|
||||
...(await vi.importActual<typeof import('react-router-dom')>('react-router-dom')),
|
||||
useNavigate: () => navigate,
|
||||
}));
|
||||
|
||||
import { api } from '../lib/api';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import DashboardPage from './DashboardPage';
|
||||
@@ -103,7 +109,32 @@ describe('DashboardPage (ported clinic dashboard)', () => {
|
||||
expect(screen.queryByText('پزشکان کلینیک')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('«مشاهده» به همان روزِ نوبت در لیست نوبتها لینک میدهد', async () => {
|
||||
it('«مشاهده» نوبتِ قطعیشده، پروندهٔ بیمار را باز میکند', async () => {
|
||||
// fixture وضعیت `completed` دارد — یعنی از قطعیشدن رد شده و پرونده دارد.
|
||||
get.mockImplementation((url: string) => {
|
||||
if (url.startsWith('/api/v1/patients?search='))
|
||||
return Promise.resolve({ success: true, data: [{ uuid: 'rec-42' }] });
|
||||
return Promise.resolve(clinicPayload);
|
||||
});
|
||||
|
||||
renderWithProviders(<DashboardPage />, { route: '/admin/dashboard' });
|
||||
await screen.findByText('دنیا خلیلی');
|
||||
|
||||
fireEvent.click(screen.getByText('مشاهده'));
|
||||
|
||||
await waitFor(() => expect(get).toHaveBeenCalledWith(expect.stringContaining('/api/v1/patients?search=09136549874')));
|
||||
await waitFor(() => expect(navigate).toHaveBeenCalledWith('/admin/patients/rec-42'));
|
||||
});
|
||||
|
||||
it('«مشاهده» نوبتِ ثبتشده (قطعینشده) به لیست نوبتهای همان روز میرود', async () => {
|
||||
get.mockResolvedValue({
|
||||
...clinicPayload,
|
||||
data: {
|
||||
...clinicPayload.data,
|
||||
today_appointments: [{ ...clinicPayload.data.today_appointments[0], status: 'pending' }],
|
||||
},
|
||||
});
|
||||
|
||||
renderWithProviders(<DashboardPage />, { route: '/admin/dashboard' });
|
||||
await screen.findByText('دنیا خلیلی');
|
||||
|
||||
|
||||
@@ -839,28 +839,8 @@ function DoctorDashboard() {
|
||||
currentJalaliMonth={chartPeriod.currentJalaliMonth}
|
||||
/>
|
||||
|
||||
<div className="grid-2" style={{ marginTop: 'var(--gap)' }}>
|
||||
<div style={{ marginTop: 'var(--gap)' }}>
|
||||
<DoctorClinicInvitationsCard />
|
||||
<div className="card card-pad">
|
||||
<div className="card-title-row">
|
||||
<h3 style={{ fontSize: 16 }}>کلینیکهای من</h3>
|
||||
</div>
|
||||
{!d?.clinics.length ? (
|
||||
<p className="muted" style={{ textAlign: 'center', padding: '32px 0', fontSize: 13.5 }}>عضو کلینیکی نیستید</p>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{d.clinics.map((c, i) => (
|
||||
<div key={c.uuid} style={{ display: 'flex', alignItems: 'center', gap: 11, padding: '10px 0', borderBottom: i < d.clinics.length - 1 ? '1px solid var(--border)' : 'none' }}>
|
||||
<AvatarEl initials={c.name.slice(0, 1)} hue={205} size="sm" />
|
||||
<div style={{ flex: 1 }}>
|
||||
<b style={{ fontSize: 13.5 }}>{c.name}</b>
|
||||
</div>
|
||||
<span className="badge green"><span className="bdot" />فعال</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user