diff --git a/assets/admin/App.tsx b/assets/admin/App.tsx index 572117a4..5265060f 100644 --- a/assets/admin/App.tsx +++ b/assets/admin/App.tsx @@ -59,7 +59,6 @@ import MyFinancialPage from './pages/MyFinancialPage'; import ClinicFormPage from './pages/ClinicFormPage'; import PreRegistrationsPage from './pages/PreRegistrationsPage'; import StaffPage from './pages/StaffPage'; -import StaffMyServicesPage from './pages/StaffMyServicesPage'; import StaffTreatmentSessionsPage from './pages/StaffTreatmentSessionsPage'; import StaffSessionDetailPage from './pages/StaffSessionDetailPage'; import TreatmentCasesPage from './pages/TreatmentCasesPage'; @@ -285,7 +284,6 @@ export default function App() { {/* فاز ۲ — دکتر / کلینیک */} } /> {/* پرسنل: تنها صفحهٔ دادهٔ این نقش، کنار داشبورد */} - } /> } /> } /> } /> diff --git a/assets/admin/components/layout/Sidebar.tsx b/assets/admin/components/layout/Sidebar.tsx index 83673c24..a3ad08b6 100644 --- a/assets/admin/components/layout/Sidebar.tsx +++ b/assets/admin/components/layout/Sidebar.tsx @@ -559,11 +559,6 @@ function buildSections( icon: ClipboardDocumentListIcon, label: "جلسات امروز من", }, - { - to: "/admin/my-services", - icon: WrenchScrewdriverIcon, - label: "سرویس‌های من", - }, ], }, ]; diff --git a/assets/admin/components/layout/SidebarStaff.test.tsx b/assets/admin/components/layout/SidebarStaff.test.tsx index 09bb7778..3fb11073 100644 --- a/assets/admin/components/layout/SidebarStaff.test.tsx +++ b/assets/admin/components/layout/SidebarStaff.test.tsx @@ -30,13 +30,16 @@ describe("Sidebar — نقش پرسنل", () => { }, } as any); - it("shows only داشبورد and سرویس‌های من", () => { + it("shows only داشبورد and جلسات امروز من", () => { asStaff(); renderWithProviders(, { route: "/admin/dashboard" }); expect(screen.getByText("داشبورد").closest("a")).toHaveAttribute("href", "/admin/dashboard"); - expect(screen.getByText("سرویس‌های من").closest("a")).toHaveAttribute("href", "/admin/my-services"); + expect(screen.getByText("جلسات امروز من").closest("a")).toHaveAttribute("href", "/admin/my-sessions"); expect(screen.getByText("پرسنل")).toBeInTheDocument(); // برچسب نقش در فوتر + + // صفحهٔ «سرویس‌های من» حذف شد؛ فهرست سرویس‌ها روی خودِ داشبورد است. + expect(screen.queryByText("سرویس‌های من")).not.toBeInTheDocument(); }); it("hides management entries", () => { diff --git a/assets/admin/pages/DashboardPage.test.tsx b/assets/admin/pages/DashboardPage.test.tsx index 50ae9117..866fce7b 100644 --- a/assets/admin/pages/DashboardPage.test.tsx +++ b/assets/admin/pages/DashboardPage.test.tsx @@ -220,14 +220,14 @@ describe('داشبورد پرسنل', () => { expect(screen.getByText('محمد رستمی')).toBeInTheDocument(); }); - it('کارت‌های آمار به صفحهٔ کار خودشان لینک‌اند', async () => { + it('کارت جلسات لینک است و کارت سرویس‌ها نیست', async () => { renderWithProviders(); const sessions = (await screen.findByText('جلسات امروز من')).closest('a'); expect(sessions).toHaveAttribute('href', '/admin/my-sessions'); - const services = screen.getByText('سرویس‌های من').closest('a'); - expect(services).toHaveAttribute('href', '/admin/my-services'); + // صفحهٔ «سرویس‌های من» حذف شد؛ کارتِ بدون مقصد نباید لینک باشد. + expect(screen.getByText('سرویس‌های من').closest('a')).toBeNull(); }); it('هر ردیف کار به همان جلسه می‌رود', async () => { diff --git a/assets/admin/pages/DashboardPage.tsx b/assets/admin/pages/DashboardPage.tsx index 75bbe538..c25454e5 100644 --- a/assets/admin/pages/DashboardPage.tsx +++ b/assets/admin/pages/DashboardPage.tsx @@ -1016,7 +1016,7 @@ function StaffDashboard() { icon: CalendarDaysIcon, color: 'var(--warning)', bg: 'var(--warning-bg)', }, { - to: '/admin/my-services', + to: null, label: 'سرویس‌های من', value: formatNumber(d.stats?.services ?? 0), hint: 'سرویس‌هایی که مجاز به انجامشان هستید', @@ -1045,16 +1045,24 @@ function StaffDashboard() {
- {kpiCards.map(c => ( - -
- -
-
{c.label}
-
{c.value}
-
{c.hint}
- - ))} + {kpiCards.map(c => { + const body = ( + <> +
+ +
+
{c.label}
+
{c.value}
+
{c.hint}
+ + ); + + // فقط کارتی لینک می‌شود که صفحه‌ای پشتش باشد؛ سرویس‌ها صفحهٔ جدا ندارند و + // همین‌جا پایین‌تر فهرست می‌شوند. + return c.to === null + ?
{body}
+ : {body}; + })}
@@ -1102,7 +1110,6 @@ function StaffDashboard() {

سرویس‌های تخصیص‌یافته

- همه سرویس‌ها
{d.services.length === 0 ? (

diff --git a/assets/admin/pages/StaffMyServicesPage.tsx b/assets/admin/pages/StaffMyServicesPage.tsx deleted file mode 100644 index 3b4c042b..00000000 --- a/assets/admin/pages/StaffMyServicesPage.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { WrenchScrewdriverIcon } from '@heroicons/react/24/outline'; -import { api } from '../lib/api'; -import type { ApiResponse } from '../lib/api'; -import type { StaffAssignedService } from '../types'; -import { formatRial } from '../lib/utils'; -import DataTable, { type Column } from '../components/ui/DataTable'; -import PageHeader from '../components/ui/PageHeader'; - -interface StaffDashboardData { - services: StaffAssignedService[]; -} - -const EMPTY: StaffAssignedService[] = []; - -/** - * سرویس‌های تخصیص‌یافته به پرسنل — فقط خواندنی. - * داده از همان اندپوینت داشبورد پرسنل می‌آید؛ نقش staff اندپوینت دیگری ندارد. - */ -export default function StaffMyServicesPage() { - const { data, isLoading } = useQuery({ - queryKey: ['dashboard-staff'], - queryFn: () => api.get>('/api/v1/dashboard/staff'), - staleTime: 60_000, - }); - - const services = data?.data?.services ?? EMPTY; - - const columns: Column[] = [ - { - key: 'name', - header: 'سرویس', - render: (s) => ( -

-
{s.name}
-
{s.section_name}
-
- ), - }, - { - key: 'price_rials', - header: 'تعرفه', - render: (s) => {formatRial(s.price_rials)}, - }, - { - key: 'duration_minutes', - header: 'مدت', - render: (s) => ( - - {s.duration_minutes ? `${s.duration_minutes} دقیقه` : '—'} - - ), - }, - ]; - - return ( - <> - - -
- {services.length === 0 && !isLoading ? ( -
- -
- هنوز سرویسی به شما تخصیص نیافته -
-
پس از تخصیص سرویس توسط مطب/کلینیک، اینجا نمایش داده می‌شود.
-
- ) : ( - - )} -
- - ); -} diff --git a/assets/admin/pages/StaffSessionDetailPage.tsx b/assets/admin/pages/StaffSessionDetailPage.tsx index c8390a41..46af7000 100644 --- a/assets/admin/pages/StaffSessionDetailPage.tsx +++ b/assets/admin/pages/StaffSessionDetailPage.tsx @@ -7,6 +7,7 @@ import PageHeader from '../components/ui/PageHeader'; import StatusBadge from '../components/ui/StatusBadge'; import SearchableSelect from '../components/ui/SearchableSelect'; import { formatDate, formatNumber } from '../lib/utils'; +import { ArrowUturnRightIcon } from '@heroicons/react/24/outline'; import { useElapsed } from '../hooks/useElapsed'; import type { SessionAreaRecord, StaffSessionDetail, TreatmentDevice, TreatmentFormField } from '../types'; @@ -59,11 +60,21 @@ export default function StaffSessionDetailPage() { }); const skipArea = useMutation({ - mutationFn: (areaUuid: string) => api.post>(`${BASE}/session-area/${areaUuid}/skip`, {}), + mutationFn: (payload: { areaUuid: string; note: string }) => + api.post>(`${BASE}/session-area/${payload.areaUuid}/skip`, { + note: payload.note || undefined, + }), onSuccess: () => { toast.success('این ناحیه صرف‌نظر شد'); refresh(); }, onError: (e) => fail(e, 'صرف‌نظر از ناحیه ناموفق بود'), }); + // اشتباهِ حین کار: ناحیه‌ای که زودتر بسته شده تا وقتی جلسه باز است برمی‌گردد. + const reopenArea = useMutation({ + mutationFn: (areaUuid: string) => api.post>(`${BASE}/session-area/${areaUuid}/reopen`, {}), + onSuccess: () => { toast.success('ناحیه دوباره باز شد'); refresh(); }, + onError: (e) => fail(e, 'باز کردن ناحیه ناموفق بود'), + }); + const completeArea = useMutation({ mutationFn: (payload: { areaUuid: string; body: Record }) => api.post>(`${BASE}/session-area/${payload.areaUuid}/complete`, payload.body), @@ -140,9 +151,11 @@ export default function StaffSessionDetailPage() { devices={session.devices} forms={session.forms} disabled={finished} - onSkip={() => skipArea.mutate(area.uuid)} + onSkip={(note) => skipArea.mutate({ areaUuid: area.uuid, note })} + onReopen={() => reopenArea.mutate(area.uuid)} onComplete={(body) => completeArea.mutate({ areaUuid: area.uuid, body })} saving={completeArea.isPending} + reopening={reopenArea.isPending} /> ))}
@@ -177,14 +190,16 @@ export default function StaffSessionDetailPage() { ); } -function AreaCard({ area, devices, forms, disabled, onSkip, onComplete, saving }: { +function AreaCard({ area, devices, forms, disabled, onSkip, onReopen, onComplete, saving, reopening }: { area: SessionAreaRecord; devices: TreatmentDevice[]; forms: Record; disabled: boolean; - onSkip: () => void; + onSkip: (note: string) => void; + onReopen: () => void; onComplete: (body: Record) => void; saving: boolean; + reopening: boolean; }) { const [open, setOpen] = useState(false); const [values, setValues] = useState>({}); @@ -192,6 +207,8 @@ function AreaCard({ area, devices, forms, disabled, onSkip, onComplete, saving } // دستگاه از نوبت به ارث می‌رسد و همان می‌ماند. این فلگ فقط برای موردِ نادرِ // «نوبت روی دستگاه اشتباه ثبت شده» است، نه بخشی از جریان عادی. const [changingDevice, setChangingDevice] = useState(false); + const [skipping, setSkipping] = useState(false); + const [skipNote, setSkipNote] = useState(''); const [resourceUuid, setResourceUuid] = useState(area.resource?.uuid ?? null); const fields = resourceUuid ? forms[resourceUuid] ?? [] : []; const settled = area.status === 'completed' || area.status === 'skipped'; @@ -241,17 +258,51 @@ function AreaCard({ area, devices, forms, disabled, onSkip, onComplete, saving } یادداشت: {area.note} )} - {!settled && !disabled && !open && ( + {/* ناحیهٔ بسته‌شده تا وقتی جلسه باز است برمی‌گردد: اپراتور وسط لیزر می‌فهمد + اشتباه زده و نباید تا پایان جلسه با آن بماند. */} + {settled && !disabled && ( +
+ +
+ )} + + {!settled && !disabled && !open && !skipping && (
-
)} + {/* «چرا انجام نشد» بخشی از سابقهٔ درمان است؛ جلسهٔ بعد باید بتوان خواندش. */} + {!settled && !disabled && skipping && ( +
+ +