diff --git a/assets/admin/components/dashboard/NewAppointmentsTable.tsx b/assets/admin/components/dashboard/NewAppointmentsTable.tsx
index b5b392d4..3218f21c 100644
--- a/assets/admin/components/dashboard/NewAppointmentsTable.tsx
+++ b/assets/admin/components/dashboard/NewAppointmentsTable.tsx
@@ -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
: }
-
- مشاهده
-
+
))}
@@ -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 مشاهده;
+ }
+
+ 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 (
+
+ );
+}
+
/** نمایش فقطخواندنی وضعیت — وقتی version یا queryKey در دسترس نیست. */
function StatusPill({ status }: { status: string }) {
const meta = STATUS_META[status] ?? { label: status, color: 'var(--text-3)' };
diff --git a/assets/admin/pages/DashboardPage.test.tsx b/assets/admin/pages/DashboardPage.test.tsx
index be4e4ca6..47414d8f 100644
--- a/assets/admin/pages/DashboardPage.test.tsx
+++ b/assets/admin/pages/DashboardPage.test.tsx
@@ -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('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(, { 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(, { route: '/admin/dashboard' });
await screen.findByText('دنیا خلیلی');
diff --git a/assets/admin/pages/DashboardPage.tsx b/assets/admin/pages/DashboardPage.tsx
index 29cca6e7..28928c20 100644
--- a/assets/admin/pages/DashboardPage.tsx
+++ b/assets/admin/pages/DashboardPage.tsx
@@ -839,28 +839,8 @@ function DoctorDashboard() {
currentJalaliMonth={chartPeriod.currentJalaliMonth}
/>
-