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:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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={[
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user