feat: implement BackButton component for consistent navigation

- Added BackButton component to standardize back navigation across pages.
- Integrated BackButton into various pages, replacing custom back buttons for consistency.
- Updated PageHeader to accept backTo prop for displaying BackButton when navigating from subpages.
- Created useGoBack hook to handle navigation logic, determining whether to go back in history or redirect to a fallback page.
- Added tests for BackButton and its integration with PageHeader to ensure expected behavior.
This commit is contained in:
hamed
2026-07-29 20:26:51 +03:30
parent e6267080b2
commit e0e8fbd1e4
31 changed files with 224 additions and 82 deletions
+13 -2
View File
@@ -139,13 +139,24 @@ Theming: **dark mode** overrides via `[data-theme="dark"]`; **compact density**
Reuse these before building new ones:
`DataTable` (sortable, search, skeleton loading, empty state, bulk) · `Modal` · `ConfirmDialog` ·
`PageHeader` (title + breadcrumb + action) · `StatCard` · `StatusBadge` · `Pagination` ·
`SearchableSelect` · `AppointmentStatusDropdown` · `PersianDateInput` / `PersianDatePicker` /
`PageHeader` (title + breadcrumb + action + `backTo`) · `BackButton` · `StatCard` · `StatusBadge` ·
`Pagination` · `SearchableSelect` · `AppointmentStatusDropdown` · `PersianDateInput` / `PersianDatePicker` /
`PersianCalendar` · `MobileInput` · `PriceInput` · `Portal` · `FeatureGate` · `Altcha` ·
`InviteDoctorModal` · `PwaInstallBanner` / `PwaLoginCard` / `NotificationMobileCard`.
Feature composites (not generic) live one level up in `components/*.tsx`.
### دکمهٔ بازگشت — الزامی در صفحات زیرمجموعه
هر صفحه‌ای که از دل صفحهٔ دیگری باز می‌شود (جزئیات، فرم ساخت/ویرایش، زیرصفحه‌های تنظیمات)
باید دکمهٔ «بازگشت» داشته باشد، با یک ظاهر و یک رفتار:
- صفحاتی که `PageHeader` دارند: فقط `backTo="/admin/…"` بدهید.
- بقیه: `<BackButton fallback="/admin/…" />` بالای هدر صفحه.
- دکمهٔ دست‌ساز نسازید — ظاهر مرجع `cp-btn-secondary` با ارتفاع ۳۶ و آیکون `ChevronRightIcon` است
(همان دکمهٔ صفحهٔ سرویس‌ها) و رفتارش در `hooks/useGoBack.ts` متمرکز است: یک قدم عقب در تاریخچهٔ
پنل، و در ورود مستقیم/رفرش (`location.key === 'default'`) رفتن به `fallback`.
---
## Conventions
@@ -1,7 +1,7 @@
import { Link } from 'react-router-dom';
import { formatDate } from '../lib/utils';
import BackButton from './ui/BackButton';
import {
ArrowLeftPH, ArrowLeftD, FilesServicePhone, FilesServiceCalendar,
ArrowLeftD, FilesServicePhone, FilesServiceCalendar,
FilesServiceNotification, FilesServiceMessage,
} from './icons/FilesServiceIcons';
@@ -11,10 +11,7 @@ interface Tag { uuid: string; name: string; color: string }
export function Breadcrumb({ name, backTo }: { name: string; backTo: string }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-2)', fontSize: 12, marginBottom: 14 }}>
<Link to={backTo} className="bg-[var(--surface)]" style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer', color: 'var(--text-2)', textDecoration: 'none' }}>
<ArrowLeftPH />
<span>بازگشت</span>
</Link>
<BackButton fallback={backTo} />
<ArrowLeftD />
<span>پرونده</span>
<ArrowLeftD />
@@ -0,0 +1,90 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { screen, fireEvent, render } from '@testing-library/react';
import { MemoryRouter, Route, Routes, Link } from 'react-router-dom';
import BackButton from './BackButton';
import PageHeader from './PageHeader';
/**
* رفتار «بازگشت» باید در همهٔ صفحات یکی باشد: یک قدم عقب در تاریخچهٔ پنل، و وقتی
* صفحه مستقیم باز شده (تاریخچه‌ای نیست) رفتن به صفحهٔ والد.
*/
function Detail({ fallback = '/admin/clinic-services' }: { fallback?: string }) {
return (
<>
<span>صفحهٔ جزئیات</span>
<BackButton fallback={fallback} />
</>
);
}
function List() {
return (
<>
<span>صفحهٔ لیست</span>
<Link to="/admin/clinic-services/x1">باز کردن جزئیات</Link>
</>
);
}
function renderAt(initialEntries: string[]) {
return render(
<MemoryRouter initialEntries={initialEntries}>
<Routes>
<Route path="/admin/clinic-services" element={<List />} />
<Route path="/admin/clinic-services/:uuid" element={<Detail />} />
</Routes>
</MemoryRouter>,
);
}
beforeEach(() => vi.clearAllMocks());
describe('BackButton', () => {
it('ظاهر یکسان دارد: همان دکمهٔ بازگشتِ صفحهٔ سرویس‌ها', () => {
renderAt(['/admin/clinic-services/x1']);
const btn = screen.getByRole('button', { name: /بازگشت/ });
expect(btn).toHaveClass('cp-btn-secondary');
expect(btn).toHaveAttribute('type', 'button');
});
it('وقتی از صفحهٔ دیگری آمده‌ایم، یک قدم به همان صفحه برمی‌گردد', () => {
renderAt(['/admin/clinic-services']);
fireEvent.click(screen.getByText('باز کردن جزئیات'));
expect(screen.getByText('صفحهٔ جزئیات')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /بازگشت/ }));
expect(screen.getByText('صفحهٔ لیست')).toBeInTheDocument();
});
it('در ورود مستقیم (بدون تاریخچه) به صفحهٔ والد می‌رود', () => {
renderAt(['/admin/clinic-services/x1']);
fireEvent.click(screen.getByRole('button', { name: /بازگشت/ }));
expect(screen.getByText('صفحهٔ لیست')).toBeInTheDocument();
});
});
describe('PageHeader — backTo', () => {
it('با backTo دکمهٔ بازگشت را نشان می‌دهد', () => {
render(
<MemoryRouter initialEntries={['/admin/claims/p1']}>
<PageHeader title="پروندهٔ بیمه" backTo="/admin/claims" />
</MemoryRouter>,
);
expect(screen.getByRole('button', { name: /بازگشت/ })).toBeInTheDocument();
});
it('بدون backTo دکمه‌ای رندر نمی‌شود (صفحات سطح‌اول)', () => {
render(
<MemoryRouter>
<PageHeader title="مطالبات بیمه" />
</MemoryRouter>,
);
expect(screen.queryByRole('button', { name: /بازگشت/ })).not.toBeInTheDocument();
});
});
+23
View File
@@ -0,0 +1,23 @@
import { ChevronRightIcon } from '@heroicons/react/24/outline';
import { useGoBack } from '../../hooks/useGoBack';
interface Props {
/** مقصد وقتی تاریخچه‌ای برای برگشتن نیست (ورود مستقیم/رفرش) — معمولاً صفحهٔ لیستِ همان بخش. */
fallback: string;
label?: string;
}
/**
* دکمهٔ «بازگشت» — یک ظاهر و یک رفتار در همهٔ صفحاتی که از دل صفحهٔ دیگری باز می‌شوند.
* ظاهر مرجع، همان دکمهٔ بازگشتِ صفحهٔ سرویس‌هاست (`cp-btn-secondary` با ارتفاع ۳۶).
* در RTL، فلشِ «قبلی» راست‌سو است.
*/
export default function BackButton({ fallback, label = 'بازگشت' }: Props) {
const goBack = useGoBack(fallback);
return (
<button type="button" className="cp-btn-secondary" style={{ height: 36 }} onClick={goBack}>
<ChevronRightIcon style={{ width: 16 }} /> {label}
</button>
);
}
+12 -1
View File
@@ -1,6 +1,7 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ChevronLeftIcon } from '@heroicons/react/24/outline';
import BackButton from './BackButton';
interface Crumb {
label: string;
@@ -12,12 +13,22 @@ interface Props {
breadcrumbs?: Crumb[];
action?: React.ReactNode;
description?: string;
/**
* صفحه از دل صفحهٔ دیگری باز می‌شود → دکمهٔ «بازگشت» بالای عنوان.
* مقدار، مقصدِ fallback است وقتی تاریخچه‌ای برای برگشتن نیست.
*/
backTo?: string;
}
export default function PageHeader({ title, breadcrumbs, action, description }: Props) {
export default function PageHeader({ title, breadcrumbs, action, description, backTo }: Props) {
return (
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, marginBottom: 24 }}>
<div style={{ minWidth: 0 }}>
{backTo && (
<div style={{ marginBottom: 10 }}>
<BackButton fallback={backTo} />
</div>
)}
{breadcrumbs && breadcrumbs.length > 0 && (
<nav style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 6, fontSize: 12, color: 'var(--text-3)' }}>
{breadcrumbs.map((crumb, i) => (
+24
View File
@@ -0,0 +1,24 @@
import { useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
/**
* رفتار یکسانِ «بازگشت» در کل پنل: اگر کاربر از صفحهٔ دیگری داخل خود پنل آمده باشد،
* یک قدم در تاریخچه برمی‌گردد؛ اگر صفحه مستقیم باز شده باشد (لینک مستقیم، رفرش،
* بوکمارک) تاریخچه‌ای برای برگشتن نیست و به صفحهٔ والدِ همان بخش می‌رود.
*
* تشخیص «ورود مستقیم» با `location.key === 'default'` انجام می‌شود — همان چیزی که
* React Router برای اولین ورودیِ تاریخچه می‌گذارد؛ `history.length` قابل اتکا نیست
* چون تب‌های قبلی مرورگر هم در آن شمرده می‌شوند.
*/
export function useGoBack(fallback: string): () => void {
const navigate = useNavigate();
const location = useLocation();
return useCallback(() => {
if (location.key !== 'default') {
navigate(-1);
return;
}
navigate(fallback, { replace: true });
}, [navigate, location.key, fallback]);
}
+2 -3
View File
@@ -14,6 +14,7 @@ import { WalletChargeLink } from '../components/AppointmentActions';
import { useDoctorBookingServices } from '../hooks/useDoctorBookingServices';
import ServiceSlotPicker from '../components/appointments/ServiceSlotPicker';
import { tehranWallClockToUnix, rialToToman, tomanToRial } from '../lib/utils';
import BackButton from '../components/ui/BackButton';
import { digitsOnly, todayIso } from '../lib/utils';
/**
@@ -166,9 +167,7 @@ export default function AppointmentCreatePage() {
<div style={{ padding: '20px 24px', maxWidth: 1080, margin: '0 auto' }}>
{/* بردکرامب */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 18 }}>
<button onClick={() => navigate(-1)} className="btn sm" style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<ChevronRightIcon style={{ width: 15, height: 15 }} /> بازگشت
</button>
<BackButton fallback="/admin/appointments" />
<span style={{ fontSize: 13, color: 'var(--text-3)' }}>نوبت ها</span>
<span style={{ color: 'var(--text-3)' }}></span>
<span style={{ fontSize: 15, fontWeight: 700, color: 'var(--text)' }}>ثبت نوبت جدید</span>
+1 -7
View File
@@ -115,19 +115,13 @@ export default function AppointmentDetailPage() {
return (
<div>
<PageHeader
backTo="/admin/appointments"
title="جزئیات نوبت"
breadcrumbs={[
{ label: 'داشبورد', to: '/admin/dashboard' },
{ label: 'نوبت‌ها', to: backTo },
{ label: 'جزئیات' },
]}
action={
<button onClick={() => navigate(backTo)}
className="flex items-center gap-2 text-sm text-[var(--text-2)] hover:text-[var(--text)] transition-colors">
<ArrowRightIcon className="w-4 h-4" />
بازگشت
</button>
}
/>
{isLoading ? (
+2 -3
View File
@@ -11,6 +11,7 @@ import SearchableSelect from '../components/ui/SearchableSelect';
import { WalletChargeLink } from '../components/AppointmentActions';
import { formatRial, rialToToman, tomanToRial } from '../lib/utils';
import { DEFAULT_SERVICE_CATEGORY } from '../lib/insuranceShares';
import BackButton from '../components/ui/BackButton';
import { useAppointmentInsurance } from '../hooks/useAppointmentInsurance';
interface Option { uuid: string; name?: string; full_name?: string }
@@ -140,9 +141,7 @@ export default function AppointmentEditPage() {
return (
<div className="fade-in" style={{ maxWidth: 860, margin: '0 auto' }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 14 }}>
<Link to={`/admin/appointments?date=${date}`} className="btn sm ghost" style={{ color: 'var(--text-2)' }}>
<ChevronRightIcon style={{ width: 16 }} /> بازگشت
</Link>
<BackButton fallback={`/admin/appointments?date=${date}`} />
</div>
<div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r-lg)', padding: 22 }}>
+1
View File
@@ -139,6 +139,7 @@ export default function BlogFormPage() {
return (
<div>
<PageHeader
backTo="/admin/blogs"
title={isEdit ? 'ویرایش مقاله' : 'نوشتن مقاله جدید'}
breadcrumbs={[
{ label: 'داشبورد', to: '/admin/dashboard' },
@@ -157,6 +157,7 @@ export default function ClaimPatientDetailPage() {
return (
<FeatureGate feature="insurance">
<PageHeader
backTo="/admin/claims"
title={patient?.full_name ?? 'پرونده بیمه بیمار'}
description={[patient?.mobile, patient?.national_code].filter(Boolean).join(' · ') || undefined}
breadcrumbs={[
@@ -9,6 +9,7 @@ import { usePermissions } from '../hooks/usePermissions';
import SettingsLayout from '../components/layout/SettingsLayout';
import { ScheduleSection } from '../components/schedule/ScheduleSection';
import FreeVisitPrice from '../components/FreeVisitPrice';
import BackButton from '../components/ui/BackButton';
import type { ClinicDoctorItem } from '../components/ClinicDoctorsManager';
/**
@@ -56,6 +57,9 @@ function ClinicAppointmentSettingsContent() {
return (
<div className="fade-in" style={{ display: 'flex', flexDirection: 'column', gap: 'var(--gap)' }}>
<div>
<BackButton fallback="/admin/settings-menu" />
</div>
<div className="card-title-row">
<div>
<h1 className="section-title">مدیریت نوبت دهی</h1>
+1 -3
View File
@@ -578,13 +578,11 @@ export default function ClinicDetailPage() {
{/* ── Header ── */}
<PageHeader
backTo="/admin/clinics"
title={clinicName}
breadcrumbs={[{ label: 'کلینیک‌ها', to: '/admin/clinics' }, { label: clinicName }]}
action={
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
<button className="btn ghost sm" onClick={() => navigate('/admin/clinics')}>
<ArrowRightIcon style={{ width: 15, height: 15 }} /> بازگشت
</button>
{!isReadOnly && (
<button className="btn soft sm" onClick={() => openEdit()}>
<PencilIcon style={{ width: 15, height: 15 }} /> ویرایش
+4
View File
@@ -4,6 +4,7 @@ import { PencilIcon } from '@heroicons/react/24/outline';
import { useAuthStore } from '../stores/authStore';
import { usePermissions } from '../hooks/usePermissions';
import SettingsLayout from '../components/layout/SettingsLayout';
import BackButton from '../components/ui/BackButton';
import ClinicDoctorsManager from '../components/ClinicDoctorsManager';
/**
@@ -44,6 +45,9 @@ function ClinicDoctorsContent() {
return (
<div className="fade-in" style={{ display: 'flex', flexDirection: 'column', gap: 'var(--gap)' }}>
<div>
<BackButton fallback="/admin/settings-menu" />
</div>
<div className="card-title-row">
<div>
<h1 className="section-title">پزشکان کلینیک</h1>
+2 -3
View File
@@ -10,6 +10,7 @@ import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import MobileInput from '../components/ui/MobileInput';
import { iranMobileSchema } from '../lib/utils';
import BackButton from '../components/ui/BackButton';
import { latinDigitsField } from '../lib/forms';
const schema = z.object({
@@ -39,9 +40,7 @@ export default function ClinicFormPage() {
return (
<div className="page" style={{ maxWidth: 640 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 28 }}>
<button className="btn ghost sm" onClick={() => navigate('/admin/clinics')}>
<ArrowRightIcon style={{ width: 16, height: 16 }} />بازگشت
</button>
<BackButton fallback="/admin/clinics" />
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<BuildingOffice2Icon style={{ width: 22, height: 22, color: 'var(--primary)' }} />
<h1 style={{ margin: 0, fontSize: 20, fontWeight: 700 }}>افزودن کلینیک جدید</h1>
+3
View File
@@ -35,6 +35,7 @@ import ImageCropModal from '../components/ImageCropModal';
import { ScheduleSection } from '../components/schedule/ScheduleSection';
import type { AddressData } from '../components/schedule/ScheduleSection';
import { latinDigitsField } from '../lib/forms';
import BackButton from '../components/ui/BackButton';
import { toggleSpecialtyChild, toggleSpecialtyRoot, removeSpecialtyEntry } from '../lib/specialtySelection';
// Fix leaflet default marker icons
@@ -1378,6 +1379,8 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
return (
<div className="animate-slide-up space-y-5">
<BackButton fallback={isOwnProfile ? '/admin/dashboard' : '/admin/doctors'} />
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-sm text-[var(--text-2)]">
{isOwnProfile ? (
+5
View File
@@ -16,6 +16,7 @@ import SearchableSelect from '../components/ui/SearchableSelect';
import MobileInput from '../components/ui/MobileInput';
import PersianDatePicker from '../components/ui/PersianDatePicker';
import { iranMobileSchema } from '../lib/utils';
import BackButton from '../components/ui/BackButton';
import { useAuthStore } from '../stores/authStore';
// ── Types ────────────────────────────────────────────────────────────────────
@@ -307,6 +308,10 @@ export default function DoctorFormPage() {
return (
<div style={{ maxWidth: 680, margin: '0 auto' }} className="animate-slide-up">
<div style={{ marginBottom: 14 }}>
<BackButton fallback="/admin/doctors" />
</div>
{/* Breadcrumb */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: 'var(--text-3)', marginBottom: 20 }}>
<button onClick={() => navigate('/admin/doctors')} style={{ display: 'flex', alignItems: 'center', gap: 4, background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-3)', fontSize: 13, padding: 0 }}>
+3 -13
View File
@@ -7,7 +7,8 @@ import type { PatientRecord } from '../types';
import type { SessionCardData } from '../components/SessionServiceCard';
import CreateStep from '../components/session/CreateStep';
import PaymentStep from '../components/session/PaymentStep';
import { ArrowLeftPH, ArrowLeftD, CloseModalD } from '../components/icons/FilesServiceIcons';
import { CloseModalD } from '../components/icons/FilesServiceIcons';
import { Breadcrumb } from '../components/PatientCaseBanner';
/** ویرایش مراجعه‌ی ثبت‌شده — دو تب: ویرایش سرویس‌ها + مدیریت پرداخت‌ها. */
export default function EditSessionPage() {
@@ -41,18 +42,7 @@ export default function EditSessionPage() {
return (
<div className="fade-in" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 30 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-2)', fontSize: 12, padding: '0 16px' }}>
<div onClick={() => nav(-1)} className="bg-[var(--surface)]" style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer' }}>
<ArrowLeftPH style={{ width: 18, height: 18, rotate: '180deg' }} />
<span>بازگشت</span>
</div>
<ArrowLeftD />
<span>پرونده</span>
<ArrowLeftD />
<span style={{ color: 'var(--text)' }}>{patientName}</span>
</div>
</div>
<Breadcrumb name={patientName} backTo={`/admin/patients/${recordUuid}`} />
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
<div className="bg-[var(--surface)]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
@@ -127,6 +127,7 @@ export default function MyPaymentDetailPage() {
return (
<>
<PageHeader
backTo="/admin/my-payments"
title="پرداخت‌های ثبت‌شده"
description="صورتحساب‌های ثبت‌شده‌ی این بیمار"
breadcrumbs={[
+3 -18
View File
@@ -9,7 +9,8 @@ import SessionStepper from '../components/SessionStepper';
import CreateStep from '../components/session/CreateStep';
import PaymentStep from '../components/session/PaymentStep';
import DetailsStep from '../components/session/DetailsStep';
import { ArrowLeftPH, ArrowLeftD, CloseModalD } from '../components/icons/FilesServiceIcons';
import { CloseModalD } from '../components/icons/FilesServiceIcons';
import { Breadcrumb } from '../components/PatientCaseBanner';
/**
* ثبت مراجعه جدید — پورت کامل tauri /files/create-service (حالت ایجاد):
@@ -52,23 +53,7 @@ export default function NewSessionPage() {
return (
<div className="fade-in" style={{ width: '100%' }}>
{/* breadcrumb — tauri AddService header */}
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 30 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-2)', fontSize: 12, padding: '0 16px' }}>
<div
onClick={() => nav(-1)}
className="bg-[var(--surface)]"
style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer' }}
>
<ArrowLeftPH style={{ width: 18, height: 18, rotate: '180deg' }} />
<span>بازگشت</span>
</div>
<ArrowLeftD />
<span>پرونده</span>
<ArrowLeftD />
<span style={{ color: 'var(--text)' }}>{patientName}</span>
</div>
</div>
<Breadcrumb name={patientName} backTo={`/admin/patients/${recordUuid}`} />
{/* card — tauri width 748 centered */}
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
+2 -1
View File
@@ -12,6 +12,7 @@ import type { PatientRecord } from '../types';
import PersianDateInput from '../components/ui/PersianDateInput';
import SearchableSelect from '../components/ui/SearchableSelect';
import { numericField } from '../lib/forms';
import BackButton from '../components/ui/BackButton';
import { iranNationalCodeSchema, iranMobileSchema, unixToIso } from '../lib/utils';
const REFERRAL_OPTIONS = ['اینستاگرام', 'معرفی دوستان و آشنایان', 'جستجوی اینترنتی', 'تابلو مطب', 'سایر'];
@@ -105,7 +106,7 @@ export default function PatientRecordFormPage() {
return (
<div className="fade-in" style={{ maxWidth: 1000, margin: '0 auto' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 18 }}>
<Link to="/admin/patients" className="btn sm ghost" style={{ color: 'var(--text-2)' }}><ChevronRightIcon style={{ width: 16 }} /> بازگشت</Link>
<BackButton fallback="/admin/patients" />
<div style={{ fontSize: 14, color: 'var(--text-3)' }}>پرونده <b style={{ color: 'var(--text)' }}>{isEdit ? 'ویرایش پرونده' : 'تشکیل پرونده'}</b></div>
</div>
+1
View File
@@ -59,6 +59,7 @@ export default function PaymentDetailPage() {
return (
<div>
<PageHeader
backTo="/admin/payments"
title="جزئیات پرداخت"
breadcrumbs={[
{ label: 'داشبورد', to: '/admin/dashboard' },
@@ -7,6 +7,7 @@ import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import type { MySubscriptionData } from '../types';
import { formatRial, formatDate } from '../lib/utils';
import BackButton from '../components/ui/BackButton';
import { PLAN_FEATURE_LABELS, planMetaOf } from './SubscriptionPage';
interface PaymentDetails {
@@ -61,6 +62,10 @@ export default function PaymentSuccessPage() {
return (
<div className="fade-in" style={{ maxWidth: 860, margin: '0 auto', padding: '8px 0 40px' }}>
<div style={{ marginBottom: 14 }}>
<BackButton fallback="/admin/subscription" />
</div>
{/* ── Success header ── */}
<div style={{ textAlign: 'center', marginBottom: 24 }}>
<div style={{
@@ -93,6 +93,7 @@ export default function RepresentationBlogFormPage() {
return (
<div>
<PageHeader
backTo="/admin/representation-blogs"
title={isEdit ? 'ویرایش مقاله' : 'نوشتن مقالهٔ جدید'}
breadcrumbs={[{ label: 'وبلاگ من', to: '/admin/representation-blogs' }, { label: isEdit ? 'ویرایش' : 'جدید' }]}
/>
@@ -184,6 +184,7 @@ export default function RepresentationDetailPage() {
return (
<div>
<PageHeader
backTo="/admin/representations"
title="جزئیات نماینده"
breadcrumbs={[
{ label: 'داشبورد', to: '/admin/dashboard' },
@@ -206,6 +207,7 @@ export default function RepresentationDetailPage() {
return (
<div>
<PageHeader
backTo="/admin/representations"
title="نماینده یافت نشد"
breadcrumbs={[
{ label: 'داشبورد', to: '/admin/dashboard' },
@@ -224,6 +226,7 @@ export default function RepresentationDetailPage() {
return (
<div>
<PageHeader
backTo="/admin/representations"
title={name}
breadcrumbs={[
{ label: 'داشبورد', to: '/admin/dashboard' },
@@ -15,6 +15,7 @@ import AppointmentStatusDropdown from '../components/ui/AppointmentStatusDropdow
import SearchableSelect from '../components/ui/SearchableSelect';
import Pagination from '../components/ui/Pagination';
import NewAppointmentDrawer from '../components/NewAppointmentDrawer';
import BackButton from '../components/ui/BackButton';
import { AppointmentInfoModal, TransferReserveModal } from '../components/AppointmentActions';
const LIMIT = 20;
@@ -115,6 +116,9 @@ export default function ReserveAppointmentsPage() {
return (
<div className="fade-in" style={{ padding: '20px 24px' }}>
<div style={{ marginBottom: 14 }}>
<BackButton fallback="/admin/appointments" />
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 10, marginBottom: 16 }}>
<h1 style={{ fontSize: 17, fontWeight: 800 }}>نوبت های رزرو شده</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+1 -1
View File
@@ -69,7 +69,7 @@ export default function SecretaryDetailPage() {
</Link>
</div>
<PageHeader title={secretary.user_name} description="جزئیات منشی و سهم درآمد نوبت‌های آنلاین" />
<PageHeader backTo="/admin/secretaries" title={secretary.user_name} description="جزئیات منشی و سهم درآمد نوبت‌های آنلاین" />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 'var(--gap)' }}>
<div className="card" style={{ padding: 20 }}>
+1
View File
@@ -494,6 +494,7 @@ function ServiceDetailPageInner() {
return (
<div className="fade-in">
<PageHeader
backTo="/admin/clinic-services"
title={item.name}
breadcrumbs={[
{ label: 'سرویس‌ها', to: '/admin/clinic-services' },
+3 -18
View File
@@ -8,7 +8,8 @@ import type { SessionCardData } from '../components/SessionServiceCard';
import SessionStepper from '../components/SessionStepper';
import PaymentStep from '../components/session/PaymentStep';
import DetailsStep from '../components/session/DetailsStep';
import { ArrowLeftPH, ArrowLeftD, CloseModalD } from '../components/icons/FilesServiceIcons';
import { CloseModalD } from '../components/icons/FilesServiceIcons';
import { Breadcrumb } from '../components/PatientCaseBanner';
/**
* تکمیل پرداخت مراجعه — پورت صفحه‌ی tauri /files/create-service در حالت payment:
@@ -55,23 +56,7 @@ export default function SessionPaymentPage() {
return (
<div className="fade-in" style={{ width: '100%' }}>
{/* breadcrumb — tauri AddService header */}
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 30 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-2)', fontSize: 12, padding: '0 16px' }}>
<div
onClick={() => nav(-1)}
className="bg-[var(--surface)]"
style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer' }}
>
<ArrowLeftPH style={{ width: 18, height: 18, rotate: '180deg' }} />
<span>بازگشت</span>
</div>
<ArrowLeftD />
<span>پرونده</span>
<ArrowLeftD />
<span style={{ color: 'var(--text)' }}>{patientName}</span>
</div>
</div>
<Breadcrumb name={patientName} backTo={`/admin/patients/${recordUuid}`} />
{/* card — tauri width 748 centered */}
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
+2 -3
View File
@@ -9,6 +9,7 @@ import { useAuthStore } from '../stores/authStore';
import { formatDateTime, formatRial } from '../lib/utils';
import StatusBadge from '../components/ui/StatusBadge';
import ConfirmDialog from '../components/ui/ConfirmDialog';
import BackButton from '../components/ui/BackButton';
import Modal from '../components/ui/Modal';
interface SettlementDetail {
@@ -139,9 +140,7 @@ export default function SettlementDetailPage() {
<div className="fade-in">
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<button className="btn ghost sm" onClick={() => navigate('/admin/settlements')} style={{ padding: '6px 10px' }}>
<ArrowRightIcon style={{ width: 15, height: 15 }} /> تسویهحسابها
</button>
<BackButton fallback="/admin/settlements" />
<div>
<h1 className="section-title">جزئیات تسویه</h1>
<div className="muted" style={{ fontSize: 13 }}>{s.representation_name}</div>
+3
View File
@@ -16,6 +16,7 @@ import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import { formatDate, formatDateTime } from '../lib/utils';
import Modal from '../components/ui/Modal';
import BackButton from '../components/ui/BackButton';
import ConfirmDialog from '../components/ui/ConfirmDialog';
// ── Types ─────────────────────────────────────────────────────────────────
@@ -333,6 +334,8 @@ export default function UserDetailPage() {
return (
<div className="animate-slide-up space-y-5">
<BackButton fallback="/admin/users" />
{/* ── Breadcrumb / Back ─────────────────────────────────── */}
<div className="flex items-center gap-2 text-sm text-[var(--text-2)]">
<button