feat(tour): add onboarding tours for various admin pages

- Integrated TourButton component into SettingsMenuPage, SkillsPage, SmsWalletPage, StaffPage, StaffSessionDetailPage, StaffTreatmentSessionsPage, SubscriptionPage, TagsSettingsPage, TreatmentCasesPage to enhance user onboarding experience.
- Created new tour definitions for appointments, clinics, staff management, financial management, and patient management, ensuring comprehensive guidance for users navigating the admin panel.
- Updated documentation to reflect the addition of tours and their implementation details.
This commit is contained in:
hamed
2026-08-10 10:10:20 +03:30
parent 20c8eaaad9
commit ecdefa3c24
73 changed files with 1008 additions and 76 deletions
+2 -1
View File
@@ -8,6 +8,7 @@ import { toast } from 'sonner';
import { api } from '../lib/api';
import { useAuthStore } from '../stores/authStore';
import SettingsLayout from '../components/layout/SettingsLayout';
import TourButton from '../components/ui/TourButton';
const ROLE_LABELS: Record<string, string> = {
admin: 'مدیر', clinic: 'کلینیک', doctor: 'پزشک',
@@ -48,7 +49,7 @@ export default function AccountSettingsPage() {
return (
<SettingsLayout active="account">
<div className="fade-in" style={{ display: 'flex', flexDirection: 'column', gap: 20, maxWidth: 620 }}>
<h1 className="section-title">حساب کاربری</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title"><h1 className="section-title">حساب کاربری</h1><TourButton tourId="account-settings" ready /></div>
{/* Profile summary */}
<div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r-lg)', padding: 20 }}>
+10 -5
View File
@@ -19,6 +19,8 @@ import ServiceSlotPicker from '../components/appointments/ServiceSlotPicker';
import SlotPicker, { type PickedSlot } from '../components/appointments/SlotPicker';
import { tehranWallClockToUnix, rialToToman, tomanToRial } from '../lib/utils';
import BackButton from '../components/ui/BackButton';
import TourButton from '../components/ui/TourButton';
import { useTour } from '../hooks/useTour';
import { digitsOnly, todayIso, formatNumber } from '../lib/utils';
import Switch from '../components/ui/Switch';
@@ -266,6 +268,8 @@ export default function AppointmentCreatePage() {
onError: (e: any) => toast.error(e.message || 'خطا در ثبت نوبت'),
});
useTour('appointment-create', { ready: true });
return (
<div style={{ padding: '20px 24px', maxWidth: 1080, margin: '0 auto' }}>
{/* بردکرامب */}
@@ -273,7 +277,8 @@ export default function AppointmentCreatePage() {
<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>
<span style={{ fontSize: 15, fontWeight: 700, color: 'var(--text)' }} data-tour="page-title">ثبت نوبت جدید</span>
<TourButton tourId="appointment-create" />
</div>
{/* فرم باید صریح بگوید این نوبت برای کدام دوره است؛ بیمارِ چنددوره‌ای بدون این،
@@ -313,7 +318,7 @@ export default function AppointmentCreatePage() {
{!isDoctor && (
<>
<div style={{ ...sectionTitle, marginTop: 0 }}>پزشک:</div>
<label style={label}>انتخاب پزشک</label>
<label style={label} data-tour="create-doctor">انتخاب پزشک</label>
<div style={{ margin: '6px 0 4px', maxWidth: 400 }}>
<SearchableSelect
options={doctorOptions}
@@ -386,7 +391,7 @@ export default function AppointmentCreatePage() {
{/* حالت جستجوی مراجعه‌کنندهٔ موجود */}
{!newPatient && (
<>
<label style={label}>انتخاب مراجعه کننده</label>
<label style={label} data-tour="create-patient">انتخاب مراجعه کننده</label>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 12, margin: '6px 0 8px' }}>
<div className="field" style={{ width: 400, maxWidth: '100%' }}>
<MagnifyingGlassIcon style={{ width: 18, height: 18, color: 'var(--text-3)', flexShrink: 0 }} />
@@ -487,7 +492,7 @@ export default function AppointmentCreatePage() {
<>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 16, marginBottom: 10 }}>
<div>
<label style={label}>بخش</label>
<label style={label} data-tour="create-service">بخش</label>
<div style={{ marginTop: 6 }}>
<SearchableSelect
inputId="appt-section-select"
@@ -702,7 +707,7 @@ export default function AppointmentCreatePage() {
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', paddingTop: 4 }}>
<button className="btn primary lg" disabled={!valid || create.isPending} onClick={() => create.mutate()}>
<button className="btn primary lg" data-tour="create-submit" disabled={!valid || create.isPending} onClick={() => create.mutate()}>
{create.isPending ? '...' : 'ثبت اطلاعات'}
</button>
</div>
+5 -3
View File
@@ -106,6 +106,8 @@ export default function AppointmentDetailPage() {
<PageHeader
backTo="/admin/appointments"
title="جزئیات نوبت"
tourId="appointment-detail"
tourReady={!isLoading}
breadcrumbs={[
{ label: 'داشبورد', to: '/admin/dashboard' },
{ label: 'نوبت‌ها', to: backTo },
@@ -122,7 +124,7 @@ export default function AppointmentDetailPage() {
) : appt ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6">
<h3 className="font-semibold text-[var(--text)] mb-4">اطلاعات بیمار</h3>
<h3 className="font-semibold text-[var(--text)] mb-4" data-tour="appt-patient">اطلاعات بیمار</h3>
<InfoRow label="نام بیمار" value={appt.patient_name} />
<InfoRow label="موبایل" value={<span dir="ltr">{appt.patient_mobile}</span>} />
<InfoRow label="پزشک" value={appt.doctor?.name ?? null} />
@@ -139,7 +141,7 @@ export default function AppointmentDetailPage() {
<AppointmentSegmentsCard appointmentUuid={appt.uuid} />
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6">
<h3 className="font-semibold text-[var(--text)] mb-4">وضعیت و اقدامات</h3>
<h3 className="font-semibold text-[var(--text)] mb-4" data-tour="appt-status">وضعیت و اقدامات</h3>
<div className="mb-4">
<p className="text-sm text-[var(--text-2)] mb-2">وضعیت فعلی:</p>
<StatusBadge type="appointment" value={appt.status} />
@@ -198,7 +200,7 @@ export default function AppointmentDetailPage() {
</div>
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6 lg:col-span-2">
<h3 className="font-semibold text-[var(--text)] mb-4">تاریخچه رویدادها</h3>
<h3 className="font-semibold text-[var(--text)] mb-4" data-tour="appt-events">تاریخچه رویدادها</h3>
{eventsQuery.isLoading ? (
<div className="h-6 w-40 rounded skeleton" />
) : events.length === 0 ? (
+4 -2
View File
@@ -10,6 +10,7 @@ import SearchableSelect from '../components/ui/SearchableSelect';
import { formatRial, rialToToman, tomanToRial } from '../lib/utils';
import { DEFAULT_SERVICE_CATEGORY } from '../lib/insuranceShares';
import BackButton from '../components/ui/BackButton';
import TourButton from '../components/ui/TourButton';
import { useAppointmentInsurance } from '../hooks/useAppointmentInsurance';
import { useDoctorBookingServices } from '../hooks/useDoctorBookingServices';
import ServiceSlotPicker from '../components/appointments/ServiceSlotPicker';
@@ -194,12 +195,13 @@ export default function AppointmentEditPage() {
return (
<div className="fade-in" style={{ maxWidth: 860, margin: '0 auto' }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 14 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
<div data-tour="page-title"><TourButton tourId="appointment-edit" ready /></div>
<BackButton fallback={`/admin/appointments?date=${date}`} />
</div>
<div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r-lg)', padding: 22 }}>
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 12 }}>مشخصات سرویس:</div>
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 12 }} data-tour="edit-service">مشخصات سرویس:</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 12, marginBottom: 18 }}>
<div>
<label style={label}>بخش</label>
@@ -9,6 +9,7 @@ import { ScheduleSection } from '../components/schedule/ScheduleSection';
import type { AddressData } from '../components/schedule/ScheduleSection';
import SearchableSelect from '../components/ui/SearchableSelect';
import { usePermissions } from '../hooks/usePermissions';
import TourButton from '../components/ui/TourButton';
const PERSONAL = 'personal';
@@ -75,7 +76,7 @@ export default function AppointmentSettingsPage() {
style={{ background: 'var(--surface)', minWidth: 0 }}
className="px-4 py-4 md:px-6 md:py-6 rounded-[var(--r-lg)]"
>
<h1 className="section-title" style={{ marginBottom: 16 }}>مدیریت نوبت دهی</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title"><h1 className="section-title" style={{ marginBottom: 16 }}>مدیریت نوبت دهی</h1><TourButton tourId="appointment-settings" ready /></div>
<FreeVisitPrice readOnly={apptReadOnly} />
@@ -68,6 +68,7 @@ export default function CatalogCategoriesPage() {
<div className="fade-in">
<PageHeader
title="دسته‌بندی‌ها"
tourId="service-categories"
description="دسته‌بندی سراسری کلینیک؛ یک بار تعریف می‌شود و سرویس‌ها و منابع از همین‌ها انتخاب می‌کنند."
backTo="/admin/settings-menu"
action={
@@ -158,6 +158,7 @@ export default function ClaimPatientDetailPage() {
<FeatureGate feature="insurance">
<PageHeader
backTo="/admin/claims"
tourId="claim-detail"
title={patient?.full_name ?? 'پرونده بیمه بیمار'}
description={[patient?.mobile, patient?.national_code].filter(Boolean).join(' · ') || undefined}
breadcrumbs={[
@@ -202,7 +203,8 @@ export default function ClaimPatientDetailPage() {
/>
</div>
<Modal open={!!selected} title="جزئیات مطالبه" size="lg" onClose={() => setSelected(null)}>
<Modal open={!!selected} title="جزئیات مطالبه"
size="lg" onClose={() => setSelected(null)}>
{selected && (
<div dir="rtl" style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 12, fontSize: 13 }}>
+1
View File
@@ -183,6 +183,7 @@ export default function ClaimsPage() {
<FeatureGate feature="insurance">
<PageHeader
title="پرونده‌های بیمه"
tourId="claims"
description="پیگیری مطالبات بیمه به تفکیک بیمار"
breadcrumbs={[{ label: 'داشبورد', to: '/admin' }, { label: 'پرونده‌های بیمه' }]}
/>
@@ -87,6 +87,7 @@ function ClinicAppointmentSettingsContent() {
در دید، بدون اسکرول. پایین‌تر از هدر، کاربر باید دنبالش می‌گشت. */}
<PageHeader
title="مدیریت نوبت‌دهی"
tourId="clinic-appointment-settings"
description={description}
backTo="/admin/settings-menu"
action={(
+1
View File
@@ -587,6 +587,7 @@ export default function ClinicDetailPage() {
<PageHeader
backTo="/admin/clinics"
title={clinicName}
tourId="clinic-detail"
breadcrumbs={[{ label: 'کلینیک‌ها', to: '/admin/clinics' }, { label: clinicName }]}
action={
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
+1
View File
@@ -48,6 +48,7 @@ function ClinicDoctorsContent() {
{/* عنوان و بازگشت و اکشن هر سه در PageHeader — همان قاعدهٔ بقیهٔ صفحات پنل. */}
<PageHeader
title="پزشکان کلینیک"
tourId="clinic-doctors"
description="مدیریت پزشکان و دعوت‌نامه‌های کلینیک"
backTo="/admin/settings-menu"
action={
@@ -133,6 +133,7 @@ function ClinicServicesPageInner() {
که نه دکمهٔ بازگشت دارد و نه می‌گوید چیست. */}
<PageHeader
title="سرویس‌ها"
tourId="clinic-services"
description="بخش‌ها و سرویس‌های قابل رزرو. مدت و قیمت هر سرویس از همین‌جا می‌آید."
backTo="/admin/settings-menu"
action={
+2 -1
View File
@@ -25,6 +25,7 @@ import Pagination from '../components/ui/Pagination';
import ConfirmDialog from '../components/ui/ConfirmDialog';
import ChangeLoginMobileModal from '../components/ChangeLoginMobileModal';
import { latinDigitsField } from '../lib/forms';
import TourButton from '../components/ui/TourButton';
const HUES_LIST = [256, 205, 162, 295, 272];
@@ -108,7 +109,7 @@ export default function ClinicsPage() {
<div className="fade-in">
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
<div>
<h1 className="section-title">کلینیکها</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title"><h1 className="section-title">کلینیکها</h1><TourButton tourId="clinics" ready /></div>
<div className="muted">{formatNumber(total)} کلینیک ثبتشده</div>
</div>
<button className="btn primary sm" onClick={() => setAddOpen(true)}>
+2 -1
View File
@@ -22,6 +22,7 @@ import DoctorAppointmentsPanel from '../components/dashboard/DoctorAppointmentsP
import { usePermissions } from '../hooks/usePermissions';
import { STATUS_META } from '../components/ui/AppointmentStatusDropdown';
import StatusBadge from '../components/ui/StatusBadge';
import TourButton from '../components/ui/TourButton';
// ── Chart period (Jalali) ─────────────────────────────────────────────────
@@ -437,7 +438,7 @@ function AdminDashboard() {
<div className="fade-in">
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
<div>
<h1 className="section-title">داشبورد مدیریت</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title"><h1 className="section-title">داشبورد مدیریت</h1><TourButton tourId="dashboard" ready /></div>
<div className="muted" style={{ fontSize: 13, marginTop: 2 }}>{today} · نمای کلی عملکرد مجموعه</div>
</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
+6 -2
View File
@@ -1,5 +1,6 @@
import SettingsLayout from '../components/layout/SettingsLayout';
import DiscountTab from '../components/DiscountTab';
import TourButton from '../components/ui/TourButton';
/** مدیریت تخفیف‌ها — بخش تنظیمات برای تعریف قوانین تخفیف عمومی (owner-scoped). */
export default function DiscountsPage() {
@@ -9,8 +10,11 @@ export default function DiscountsPage() {
style={{ background: 'var(--surface)', minWidth: 0 }}
className="px-4 py-4 md:px-6 md:py-6 rounded-[var(--r-lg)]"
>
<h1 className="section-title" style={{ marginBottom: 16 }}>مدیریت تخفیفها</h1>
<DiscountTab />
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 16 }} data-tour="page-title">
<h1 className="section-title">مدیریت تخفیفها</h1>
<TourButton tourId="discounts" ready />
</div>
<div data-tour="discounts-rules"><DiscountTab /></div>
</div>
</SettingsLayout>
);
+5 -1
View File
@@ -38,6 +38,7 @@ import { latinDigitsField } from '../lib/forms';
import BackButton from '../components/ui/BackButton';
import { toggleSpecialtyChild, toggleSpecialtyRoot, removeSpecialtyEntry } from '../lib/specialtySelection';
import Switch from '../components/ui/Switch';
import TourButton from '../components/ui/TourButton';
// Fix leaflet default marker icons
delete (L.Icon.Default.prototype as any)._getIconUrl;
@@ -1432,7 +1433,10 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
</div>
<div className="flex-1 min-w-0 sm:mb-1">
<h1 className="text-xl font-bold text-[var(--text)]">{displayDoctorName(doctor.name)}</h1>
<div className="flex items-center gap-1" data-tour="page-title">
<h1 className="text-xl font-bold text-[var(--text)]">{displayDoctorName(doctor.name)}</h1>
<TourButton tourId="doctor-detail" ready />
</div>
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
{/* Active status badge */}
{doctor.active
+4 -2
View File
@@ -18,6 +18,7 @@ import PersianDatePicker from '../components/ui/PersianDatePicker';
import { iranMobileSchema } from '../lib/utils';
import BackButton from '../components/ui/BackButton';
import { useAuthStore } from '../stores/authStore';
import TourButton from '../components/ui/TourButton';
// ── Types ────────────────────────────────────────────────────────────────────
@@ -319,10 +320,11 @@ export default function DoctorFormPage() {
پزشکان
</button>
<span>/</span>
<span style={{ color: 'var(--text-2)', fontWeight: 600 }}>افزودن پزشک جدید</span>
<span style={{ color: 'var(--text-2)', fontWeight: 600 }} data-tour="page-title">افزودن پزشک جدید</span>
<TourButton tourId="doctor-new" ready />
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleSubmit(onSubmit)} data-tour="doctor-form">
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* ── Section: اطلاعات حساب ──────────────────────────────── */}
+2 -1
View File
@@ -16,6 +16,7 @@ import ChangeLoginMobileModal from '../components/ChangeLoginMobileModal';
import Pagination from '../components/ui/Pagination';
import SearchableSelect from '../components/ui/SearchableSelect';
import { useAuthStore } from '../stores/authStore';
import TourButton from '../components/ui/TourButton';
// ── Types ─────────────────────────────────────────────────────────────────
@@ -239,7 +240,7 @@ export default function DoctorsPage() {
{/* Header */}
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
<div>
<h1 className="section-title">پزشکان</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title"><h1 className="section-title">پزشکان</h1><TourButton tourId="doctors" ready /></div>
<div className="muted" style={{ fontSize: 13, marginTop: 3 }}>مدیریت پزشکان، تخصصها و وضعیت همکاری</div>
</div>
<div style={{ display: 'flex', gap: 10 }}>
+5 -1
View File
@@ -9,6 +9,7 @@ import CreateStep from '../components/session/CreateStep';
import PaymentStep from '../components/session/PaymentStep';
import { CloseModalD } from '../components/icons/FilesServiceIcons';
import { Breadcrumb } from '../components/PatientCaseBanner';
import TourButton from '../components/ui/TourButton';
/** ویرایش مراجعه‌ی ثبت‌شده — دو تب: ویرایش سرویس‌ها + مدیریت پرداخت‌ها. */
export default function EditSessionPage() {
@@ -47,7 +48,10 @@ export default function EditSessionPage() {
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
<div className="bg-[var(--surface)]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<h2 style={{ fontSize: 16, fontWeight: 700 }}>ویرایش مراجعه</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title">
<h2 style={{ fontSize: 16, fontWeight: 700 }}>ویرایش مراجعه</h2>
<TourButton tourId="session-edit" ready />
</div>
<button type="button" aria-label="بستن" onClick={() => nav(-1)}
style={{ minWidth: 40, height: 40, borderRadius: '50%', border: 'none', background: 'transparent', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<CloseModalD />
@@ -56,6 +56,7 @@ export default function HolidaysSettingsPage() {
<div className="fade-in">
<PageHeader
title="تعطیلات رسمی"
tourId="holidays"
description="تعطیلات کشوری برای همهٔ محیط‌ها اعمال می‌شود. اگر این محیط روزی را باز است، همین‌جا استثنا بزنید."
backTo="/admin/settings-menu"
/>
@@ -11,6 +11,7 @@ export default function InsurancePricingPage() {
<div className="fade-in">
<PageHeader
title="مدیریت بیمه"
tourId="insurance-pricing"
description="قراردادهای بیمه پایه و تکمیلی"
/>
<InsuranceServiceCategoriesCard />
+7 -3
View File
@@ -5,6 +5,7 @@ import SearchableSelect from '../components/ui/SearchableSelect';
import { useInventory } from '../hooks/useInventory';
import type { InventoryItem, InventoryPackage, ItemPayload, PackagePayload } from '../hooks/useInventory';
import InventoryStatCards from '../components/inventory/InventoryStatCards';
import TourButton from '../components/ui/TourButton';
import InventoryItemsTable from '../components/inventory/InventoryItemsTable';
import PackagesView from '../components/inventory/PackagesView';
import AddItemModal from '../components/inventory/AddItemModal';
@@ -56,8 +57,11 @@ export default function InventoryPage() {
<div className="fade-in" style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Header */}
<div>
<h1 style={{ fontSize: 18, fontWeight: 800, color: 'var(--text)', marginBottom: 12 }}>انبارداری</h1>
<div className="inv-tabs">
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 12 }} data-tour="page-title">
<h1 style={{ fontSize: 18, fontWeight: 800, color: 'var(--text)' }}>انبارداری</h1>
<TourButton tourId="inventory" ready />
</div>
<div className="inv-tabs" data-tour="inventory-tabs">
<button className={`inv-tab${tab === 'stock' ? ' active' : ''}`} onClick={() => setTab('stock')}>کالاهای مصرفی</button>
<button className={`inv-tab${tab === 'packages' ? ' active' : ''}`} onClick={() => setTab('packages')}>پکیج</button>
</div>
@@ -105,7 +109,7 @@ export default function InventoryPage() {
{/* Body */}
{tab === 'stock' ? (
<>
<InventoryStatCards stats={stats} />
<div data-tour="inventory-stats"><InventoryStatCards stats={stats} /></div>
{itemsLoading ? (
<div style={{ padding: 16, color: 'var(--text-3)', fontSize: 13 }}>در حال بارگذاری...</div>
) : filteredItems.length === 0 ? (
+1
View File
@@ -59,6 +59,7 @@ function MyFinancialPageContent() {
<div className="page">
<PageHeader
title="مدیریت پرداخت"
tourId="my-financial"
description={`روش‌های پرداخت «${environmentName}» (حساب بانکی و کارت‌خوان)`}
/>
+7
View File
@@ -606,6 +606,8 @@ function MyPatientsPageInner() {
<PageHeader
title="پرونده بیماران"
description="مراجعه‌کنندگان ثبت‌شده شما"
tourId="my-patients"
tourReady={!isLoading}
action={
canCreate ? (
<button
@@ -621,6 +623,7 @@ function MyPatientsPageInner() {
<div style={{ marginBottom: 16 }}>
<div
data-tour="patients-search"
style={{
background: "var(--surface)",
border: "1px solid var(--border)",
@@ -709,6 +712,7 @@ function MyPatientsPageInner() {
) : (
<>
<div
data-tour="patients-records"
style={
viewMode === "grid"
? { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: 14 }
@@ -1025,6 +1029,7 @@ function MyPatientsPageInner() {
<PageHeader
title={selectedPatientName}
description={`تلفن: ${selectedPatientPhone} — پرونده از ${formatDate(selectedRecord.created_at)}`}
tourId="my-patient-record"
action={
<div style={{ display: "flex", gap: 8 }}>
<button
@@ -1047,6 +1052,7 @@ function MyPatientsPageInner() {
{/* بنر اطلاعات بیمار — مطابق فیگما */}
<div
data-tour="record-banner"
style={{
background: "var(--surface)",
border: "1px solid var(--border)",
@@ -1130,6 +1136,7 @@ function MyPatientsPageInner() {
<div
className="cp-tabs"
data-tour="record-tabs"
style={{
display: "flex",
gap: 8,
@@ -70,7 +70,9 @@ describe('MyPaymentDetailPage (پرداخت‌های ثبت‌شده)', () => {
it('fetches invoices for the patient uuid from the route', async () => {
renderPage();
await screen.findAllByText('دنیا خلیلی');
expect(get.mock.calls[0][0]).toContain('/api/v1/my/billing/patients/abc/invoices');
// ترتیب درخواست‌ها قرارداد نیست — صفحه کوئری‌های جانبی هم دارد (مثل وضعیت تور راهنما).
const urls = get.mock.calls.map((c: unknown[]) => String(c[0]));
expect(urls.some((u) => u.includes('/api/v1/my/billing/patients/abc/invoices'))).toBe(true);
});
it('expands a row to show its line items and payment methods on بیشتر', async () => {
@@ -128,6 +128,7 @@ export default function MyPaymentDetailPage() {
<>
<PageHeader
backTo="/admin/my-payments"
tourId="my-payment-detail"
title="پرداخت‌های ثبت‌شده"
description="صورتحساب‌های ثبت‌شده‌ی این بیمار"
breadcrumbs={[
+1
View File
@@ -129,6 +129,7 @@ export default function MyPaymentsPage() {
<>
<PageHeader
title="لیست پرداخت‌ها"
tourId="my-payments"
description="پرداخت‌های ثبت‌شده‌ی بیماران شما"
breadcrumbs={[{ label: 'داشبورد', to: '/admin' }, { label: 'لیست پرداخت‌ها' }]}
/>
+7 -2
View File
@@ -13,6 +13,7 @@ import { useAuthStore } from "../stores/authStore";
import type { Secretary, SecretaryPermissions } from "../types";
import SecretaryPermissionsModal from "../components/SecretaryPermissionsModal";
import Switch from '../components/ui/Switch';
import TourButton from '../components/ui/TourButton';
// ── SVG icons (copied verbatim from clinic-pro-tauri) ───────────────────────
@@ -815,12 +816,15 @@ function MySecretariesPageContent() {
return (
<div dir="rtl">
<div className="flex items-center justify-between">
<p className="text-[var(--text)] text-[20px] font-bold">لیست منشی ها</p>
<div className="flex items-center gap-1" data-tour="page-title">
<p className="text-[var(--text)] text-[20px] font-bold">لیست منشی ها</p>
<TourButton tourId="my-secretaries" ready />
</div>
</div>
{/* تب‌ها + دکمه افزودن */}
<div className="w-full flex items-end justify-between mt-[20px]">
<div className="flex items-center gap-[8px] border-b border-[var(--border)]">
<div className="flex items-center gap-[8px] border-b border-[var(--border)]" data-tour="secretaries-tabs">
{["منشی های فعلی", "منشی های قبلی"].map((label, idx) => (
<button
key={idx}
@@ -837,6 +841,7 @@ function MySecretariesPageContent() {
))}
</div>
<button
data-tour="secretaries-add"
onClick={handleAddClick}
className="shadow-none gap-[8px] bg-[var(--primary)] text-[var(--on-primary)] text-[14px] md:text-[15px] lg:text-[16px]
font-medium py-[10px] px-[16px] h-[43px] md:h-[45px] lg:h-[48px] rounded-[4px] cursor-pointer
+6 -2
View File
@@ -11,6 +11,7 @@ import PaymentStep from '../components/session/PaymentStep';
import DetailsStep from '../components/session/DetailsStep';
import { CloseModalD } from '../components/icons/FilesServiceIcons';
import { Breadcrumb } from '../components/PatientCaseBanner';
import TourButton from '../components/ui/TourButton';
/**
* ثبت مراجعه جدید پورت کامل tauri /files/create-service (حالت ایجاد):
@@ -53,11 +54,14 @@ export default function NewSessionPage() {
return (
<div className="fade-in" style={{ width: '100%' }}>
<Breadcrumb name={patientName} backTo={`/admin/patients/${recordUuid}`} />
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title">
<Breadcrumb name={patientName} backTo={`/admin/patients/${recordUuid}`} />
<TourButton tourId="session-new" ready />
</div>
{/* card — tauri width 748 centered */}
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
<div className="bg-[var(--surface)]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
<div className="bg-[var(--surface)]" data-tour="session-card" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
<button
type="button"
+11 -2
View File
@@ -22,6 +22,8 @@ import Modal from '../components/ui/Modal';
import ConfirmDialog from '../components/ui/ConfirmDialog';
import PersianDateInput from '../components/ui/PersianDateInput';
import PatientCaseBanner, { Breadcrumb } from '../components/PatientCaseBanner';
import TourButton from '../components/ui/TourButton';
import { useTour } from '../hooks/useTour';
import SessionServiceCard, { type SessionCardData } from '../components/SessionServiceCard';
import { useIssueInvoice } from '../hooks/useIssueInvoice';
import AppointmentTurnCard, { type AppointmentCardData } from '../components/AppointmentTurnCard';
@@ -200,10 +202,16 @@ export default function PatientDetailPage() {
.filter((a: any) => (a.starts_at ?? 0) >= nowSec && !String(a.status).startsWith('cancelled'))
.sort((a: any, b: any) => a.starts_at - b.starts_at)[0]?.starts_at ?? null;
useTour('patient-detail', { ready: !isLoading });
return (
<div className="fade-in">
{/* breadcrumb + patient banner (tauri BreadcrumbHeader + FileServicesHeader) */}
<Breadcrumb name={record?.user_name || 'پرونده'} backTo="/admin/patients" />
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title">
<Breadcrumb name={record?.user_name || 'پرونده'} backTo="/admin/patients" />
<TourButton tourId="patient-detail" />
</div>
<div data-tour="patient-banner">
<PatientCaseBanner
name={record?.user_name || 'پرونده'}
recordNumber={record?.record_number}
@@ -215,9 +223,10 @@ export default function PatientDetailPage() {
hasDebt={hasDebt}
onAddNote={() => setTab('notes')}
/>
</div>
{/* tab bar */}
<div style={{ display: 'flex', gap: 4, overflowX: 'auto', borderBottom: '1px solid var(--border)', marginBottom: 18 }}>
<div data-tour="patient-tabs" style={{ display: 'flex', gap: 4, overflowX: 'auto', borderBottom: '1px solid var(--border)', marginBottom: 18 }}>
{TABS.map((t) => {
const on = t.key === tab;
return (
+6 -2
View File
@@ -15,6 +15,7 @@ import { numericField } from '../lib/forms';
import BackButton from '../components/ui/BackButton';
import { iranNationalCodeSchema, iranMobileSchema, unixToIso } from '../lib/utils';
import { useRecordNumberSettings } from '../hooks/useRecordNumberSettings';
import TourButton from '../components/ui/TourButton';
const REFERRAL_OPTIONS = ['اینستاگرام', 'معرفی دوستان و آشنایان', 'جستجوی اینترنتی', 'تابلو مطب', 'سایر'];
@@ -131,10 +132,13 @@ export default function PatientRecordFormPage() {
<div className="fade-in" style={{ maxWidth: 1000, margin: '0 auto' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 18 }}>
<BackButton fallback="/admin/patients" />
<div style={{ fontSize: 14, color: 'var(--text-3)' }}>پرونده <b style={{ color: 'var(--text)' }}>{isEdit ? 'ویرایش پرونده' : 'تشکیل پرونده'}</b></div>
<div style={{ fontSize: 14, color: 'var(--text-3)', display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title">
پرونده <b style={{ color: 'var(--text)' }}>{isEdit ? 'ویرایش پرونده' : 'تشکیل پرونده'}</b>
<TourButton tourId="patient-record-form" ready />
</div>
</div>
<form onSubmit={form.handleSubmit((d) => save.mutate(d))}
<form onSubmit={form.handleSubmit((d) => save.mutate(d))} data-tour="record-form"
style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r-lg)', padding: 24 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 18 }}>
<Field label="نام و نام خانوادگی مراجعه کننده" required error={form.formState.errors.name?.message}>
+11 -4
View File
@@ -14,6 +14,8 @@ import Pagination from '../components/ui/Pagination';
import PatientTagsCell from '../components/PatientTagsCell';
import PatientsFilterModal, { type PatientFilters } from '../components/PatientsFilterModal';
import { usePermissions } from '../hooks/usePermissions';
import { useTour } from '../hooks/useTour';
import TourButton from '../components/ui/TourButton';
import {
SearchHeaderP, TurnsFilter, PatientsGridView, PatientsCategoryView, AddTurn,
} from '../components/icons/FilesToolbarIcons';
@@ -161,6 +163,8 @@ export default function PatientsListPage() {
queryFn: () => api.get(`/api/v1/patients?${qs.toString()}`),
});
useTour('patients', { ready: !isLoading });
const activeFilters = countFilters(filters);
const records = data?.data ?? EMPTY;
const total = data?.meta?.totalRecords ?? 0;
@@ -183,14 +187,17 @@ export default function PatientsListPage() {
return (
<div className="fade-in">
{/* Head — تیتر تنها (tauri files/head) */}
<p className="text-[var(--text)] text-[16px] md:text-[18px] lg:text-[20px] font-bold">پروندهها</p>
<div className="flex items-center gap-1" data-tour="page-title">
<p className="text-[var(--text)] text-[16px] md:text-[18px] lg:text-[20px] font-bold">پروندهها</p>
<TourButton tourId="patients" />
</div>
{/* Inputs — ردیف کنترل‌ها (tauri files/inputs) */}
<div className="my-[16px] md:my-[20px] lg:my-[24px]">
<div className="flex flex-col lg:flex-row items-stretch lg:items-center gap-[16px] lg:gap-[20px]">
{/* جستجو + سوییچ نما */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 flex-1 w-full">
<div className="w-full md:w-[442px]">
<div className="w-full md:w-[442px]" data-tour="patients-search">
<div className="flex items-center rounded-[6px] border border-[var(--border)] bg-[var(--bg)]" style={{ height: 48, paddingInline: 4 }}>
<input
dir="rtl"
@@ -211,7 +218,7 @@ export default function PatientsListPage() {
{/* فیلتر + تشکیل پرونده */}
<div className="flex items-center w-full lg:w-auto justify-between lg:justify-end" style={{ gap: 12 }}>
<button
type="button" aria-label="فیلترها" onClick={() => setFilterOpen(true)}
type="button" aria-label="فیلترها" data-tour="patients-filters" onClick={() => setFilterOpen(true)}
className="flex items-center justify-center rounded-[4px] cursor-pointer"
style={{ width: 62, height: 48, border: '1px solid var(--primary)', background: 'transparent', position: 'relative' }}
>
@@ -222,7 +229,7 @@ export default function PatientsListPage() {
</button>
{canCreate && (
<button
type="button" onClick={() => navigate('/admin/patients/new')}
type="button" data-tour="patients-new" onClick={() => navigate('/admin/patients/new')}
className="flex items-center justify-center gap-2 rounded-[4px] cursor-pointer"
style={{ height: 48, minWidth: 137, background: 'var(--primary)', border: 'none', padding: '0 16px' }}
>
@@ -7,6 +7,7 @@ import SearchableSelect from '../components/ui/SearchableSelect';
import { useAuthStore } from '../stores/authStore';
import { usePermissions } from '../hooks/usePermissions';
import type { PracticeDomain } from '../types';
import TourButton from '../components/ui/TourButton';
/**
* حوزهٔ فعالیت کلینیک.
@@ -62,7 +63,7 @@ export default function PracticeDomainSettingsPage() {
// منوی تنظیمات را نشان نمی‌داد و کاربر برای رفتن به بخش بعدی باید «بازگشت» می‌زد.
<SettingsLayout active="practice-domain">
<div className="card card-pad" style={{ display: 'grid', gap: 14, maxWidth: 560 }}>
<h1 className="section-title" style={{ margin: 0 }}>حوزهٔ فعالیت</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title"><h1 className="section-title" style={{ margin: 0 }}>حوزهٔ فعالیت</h1><TourButton tourId="practice-domain" ready /></div>
<p style={{ margin: 0, fontSize: 12.5, color: 'var(--text-3)', lineHeight: 1.9 }}>
حوزهٔ فعالیت تعیین میکند سیستم چه فرآیند درمانی برای کلینیک شما اجرا کند. این با
@@ -4,6 +4,7 @@ import SearchableSelect from '../components/ui/SearchableSelect';
import { useRecordNumberSettings } from '../hooks/useRecordNumberSettings';
import type { RecordNumberResetPolicy } from '../hooks/useRecordNumberSettings';
import Switch from '../components/ui/Switch';
import TourButton from '../components/ui/TourButton';
const RESET_OPTIONS: { value: RecordNumberResetPolicy; label: string }[] = [
{ value: 'none', label: 'هرگز — شمارنده پیوسته جلو می‌رود' },
@@ -49,7 +50,7 @@ export default function RecordNumberSettingsPage() {
style={{ background: 'var(--surface)', minWidth: 0 }}
className="px-4 py-4 md:px-6 md:py-6 rounded-[var(--r-lg)]"
>
<h1 className="section-title" style={{ marginBottom: 6 }}>شماره پرونده</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title"><h1 className="section-title" style={{ marginBottom: 6 }}>شماره پرونده</h1><TourButton tourId="record-number-settings" ready /></div>
<p style={{ fontSize: 12.5, color: 'var(--text-3)', marginBottom: 18, lineHeight: 2 }}>
با روشنکردن این گزینه، شمارهٔ هر پروندهٔ تازه چه از فرم تشکیل پرونده و چه از
نوبتی که قطعی میشود بر اساس همین الگو ساخته میشود.
@@ -94,6 +94,7 @@ export default function RepresentationBlogFormPage() {
<PageHeader
backTo="/admin/representation-blogs"
title={isEdit ? 'ویرایش مقاله' : 'نوشتن مقالهٔ جدید'}
tourId="representation-blog-form"
breadcrumbs={[{ label: 'وبلاگ من', to: '/admin/representation-blogs' }, { label: isEdit ? 'ویرایش' : 'جدید' }]}
/>
@@ -53,6 +53,7 @@ export default function RepresentationBlogsPage() {
<div className="fade-in">
<PageHeader
title="وبلاگ من"
tourId="representation-blogs"
description="مقالات مخصوص دامنه و برند شما."
action={<button className="cp-btn-primary" onClick={() => navigate('/admin/representation-blogs/new')}>مقالهٔ جدید</button>}
/>
@@ -8,6 +8,7 @@ import { api } from '../lib/api';
import type { ApiResponse, PaginatedResponse } from '../lib/api';
import { formatRial, formatNumber, formatDate } from '../lib/utils';
import Pagination from '../components/ui/Pagination';
import TourButton from '../components/ui/TourButton';
interface FinanceRow {
uuid: string;
@@ -79,7 +80,7 @@ export default function RepresentationFinancePage() {
<div className="fade-in">
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
<div>
<h1 className="section-title">گزارش مالی</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title"><h1 className="section-title">گزارش مالی</h1><TourButton tourId="representation-finance" ready /></div>
<div className="muted" style={{ fontSize: 13, marginTop: 3 }}>
درآمد ثبتشده از پورسانت نوبتها تفکیک هر تراکنش
</div>
@@ -6,6 +6,7 @@ import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import PersianDatePicker from '../components/ui/PersianDatePicker';
import { toEnglishDigits, digitsOnly } from '../lib/utils';
import TourButton from '../components/ui/TourButton';
// تبدیل تاریخ میلادی ISO (YYYY-MM-DD) به شمسی Y/m/d برای استعلام api.ir
function toJalali(iso: string): string {
@@ -126,7 +127,7 @@ export default function RepresentationProfilePage() {
<div className="fade-in">
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
<div>
<h1 className="section-title">پروفایل نماینده</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title"><h1 className="section-title">پروفایل نماینده</h1><TourButton tourId="representation-profile" ready /></div>
<div className="muted" style={{ fontSize: 13, marginTop: 2 }}>تأیید هویت و مدیریت شمارههای شبا</div>
</div>
</div>
@@ -7,6 +7,7 @@ import { formatRial, formatDate, tomanToRial } from '../lib/utils';
import Pagination from '../components/ui/Pagination';
import SearchableSelect from '../components/ui/SearchableSelect';
import { digitsOnly } from '../lib/utils';
import TourButton from '../components/ui/TourButton';
interface WalletBalance { balance_rials: number }
interface RepSummary { income: { settlable_rials: number; settled_rials: number; pending_rials: number } }
@@ -101,7 +102,7 @@ export default function RepresentationSettlementPage() {
<div className="fade-in">
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
<div>
<h1 className="section-title">تسویه حساب</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title"><h1 className="section-title">تسویه حساب</h1><TourButton tourId="representation-settlement" ready /></div>
<div className="muted" style={{ fontSize: 13, marginTop: 2 }}>درخواست برداشت از کیفپول نماینده</div>
</div>
</div>
+11 -4
View File
@@ -17,6 +17,8 @@ import SearchableSelect from '../components/ui/SearchableSelect';
import Pagination from '../components/ui/Pagination';
import NewAppointmentDrawer from '../components/NewAppointmentDrawer';
import BackButton from '../components/ui/BackButton';
import TourButton from '../components/ui/TourButton';
import { useTour } from '../hooks/useTour';
import { AppointmentInfoModal, TransferReserveModal } from '../components/AppointmentActions';
const LIMIT = 20;
@@ -119,16 +121,21 @@ export default function ReserveAppointmentsPage() {
const th: React.CSSProperties = { padding: '10px 14px', textAlign: 'right', fontWeight: 600, color: 'var(--text-2)', whiteSpace: 'nowrap' };
const td: React.CSSProperties = { padding: '10px 14px', textAlign: 'right', color: 'var(--text)', verticalAlign: 'middle' };
useTour('appointments-reserve', { ready: !isLoading });
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: 4 }} data-tour="page-title">
<h1 style={{ fontSize: 17, fontWeight: 800 }}>نوبت های رزرو شده</h1>
<TourButton tourId="appointments-reserve" />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{isClinic && (
<div style={{ minWidth: 180 }}>
<div style={{ minWidth: 180 }} data-tour="reserve-doctor">
<SearchableSelect
options={clinicDoctors.map(d => ({ value: d.uuid, label: d.name }))}
value={doctorUuid || null}
@@ -140,7 +147,7 @@ export default function ReserveAppointmentsPage() {
</div>
)}
{primaryRole !== 'representation' && (
<button className="btn primary sm" disabled={!doctorUuid}
<button className="btn primary sm" data-tour="reserve-new" disabled={!doctorUuid}
onClick={() => setDrawerOpen(true)} style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<PlusIcon style={{ width: 15 }} /> نوبت رزرو
</button>
@@ -148,7 +155,7 @@ export default function ReserveAppointmentsPage() {
</div>
</div>
<div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r)', overflow: 'hidden' }}>
<div data-tour="reserve-list" style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r)', overflow: 'hidden' }}>
{isLoading ? (
<div style={{ padding: 40, textAlign: 'center', color: 'var(--text-3)' }}>در حال بارگذاری...</div>
) : items.length === 0 ? (
@@ -72,6 +72,7 @@ export default function ResourceDetailPage() {
title={resource.name}
description={`${resource.type_name}${resource.subject_kind ? ` · ${SUBJECT_LABEL[resource.subject_kind]}` : ' · تجهیزات'}`}
backTo="/admin/resources"
tourId="resource-detail"
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: resource.name }]}
action={
canUpdate ? (
+1
View File
@@ -66,6 +66,7 @@ export default function ResourcePoolsPage() {
<div className="fade-in">
<PageHeader
title="استخر منابع"
tourId="resource-pools"
description="منابعی که جایگزین کامل یکدیگرند — مثل «لیزرهای آلکساندرایت». همهٔ اعضا باید از یک نوع باشند."
backTo="/admin/resources"
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: 'استخر منابع' }]}
+1
View File
@@ -50,6 +50,7 @@ export default function ResourceTypesPage() {
<div className="fade-in">
<PageHeader
title="نوع منابع"
tourId="resource-types"
description="دسته‌بندی منابع: پزشک، پرسنل، اتاق، دستگاه لیزر، یونیت. نوع‌های سیستمی حذف نمی‌شوند چون پل خودکار منابع به آن‌ها تکیه دارد."
backTo="/admin/resources"
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: 'نوع منابع' }]}
+1
View File
@@ -104,6 +104,7 @@ export default function ResourcesPage() {
<div className="fade-in">
<PageHeader
title="منابع"
tourId="resources"
description="هر چیزی که ممکن است اشغال باشد: پزشک، اپراتور، اتاق، دستگاه. ظرفیت یعنی تعداد بیمار هم‌زمان."
/* سقف پلن پیش از باز شدن فرم گفته میشود، نه بعد از پر کردنش: خطای ۴۲۲ ته کار
همان اطلاعات را دیرتر و گرانتر میداد. تصمیم نهایی همچنان با سرور است. */
+1 -1
View File
@@ -77,7 +77,7 @@ export default function SecretaryEarningsPage() {
return (
<div className="fade-in">
<PageHeader title="درآمد نوبت‌های آنلاین" description="سهم شما از نوبت‌هایی که آنلاین ثبت و پرداخت شده‌اند" />
<PageHeader title="درآمد نوبت‌های آنلاین" description="سهم شما از نوبت‌هایی که آنلاین ثبت و پرداخت شده‌اند"tourId="secretary-earnings" />
{summary && !summary.enabled ? (
<div className="card" style={{ padding: 20, fontSize: 13, color: 'var(--text-2)', lineHeight: 1.9 }}>
@@ -109,7 +109,7 @@ export default function SecretarySettlementPage() {
return (
<div className="fade-in">
<PageHeader title="تسویه حساب" description="برداشت سهم نوبت‌های آنلاین به شماره شبای شما" />
<PageHeader title="تسویه حساب" description="برداشت سهم نوبت‌های آنلاین به شماره شبای شما"tourId="secretary-settlement" />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 'var(--gap)', marginBottom: 'var(--gap)' }}>
<StatCard label="موجودی قابل برداشت" value={formatRial(balance)} tone="green" />
+1
View File
@@ -436,6 +436,7 @@ function ServiceDetailPageInner() {
<div className="fade-in">
<PageHeader
backTo="/admin/clinic-services"
tourId="service-detail"
title={item.name}
breadcrumbs={[
{ label: 'سرویس‌ها', to: '/admin/clinic-services' },
+6 -2
View File
@@ -10,6 +10,7 @@ import PaymentStep from '../components/session/PaymentStep';
import DetailsStep from '../components/session/DetailsStep';
import { CloseModalD } from '../components/icons/FilesServiceIcons';
import { Breadcrumb } from '../components/PatientCaseBanner';
import TourButton from '../components/ui/TourButton';
/**
* تکمیل پرداخت مراجعه پورت صفحهی tauri /files/create-service در حالت payment:
@@ -56,11 +57,14 @@ export default function SessionPaymentPage() {
return (
<div className="fade-in" style={{ width: '100%' }}>
<Breadcrumb name={patientName} backTo={`/admin/patients/${recordUuid}`} />
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title">
<Breadcrumb name={patientName} backTo={`/admin/patients/${recordUuid}`} />
<TourButton tourId="session-payment" ready />
</div>
{/* card — tauri width 748 centered */}
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
<div className="bg-[var(--surface)]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
<div className="bg-[var(--surface)]" data-tour="session-card" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
<button
type="button"
+2 -1
View File
@@ -4,6 +4,7 @@ import { ChevronLeftIcon } from '@heroicons/react/24/outline';
import { groupedMenuForRole } from '../components/layout/settingsMenu';
import { useAuthStore } from '../stores/authStore';
import { usePermissions } from '../hooks/usePermissions';
import TourButton from '../components/ui/TourButton';
/**
* SettingsMenuPage the settings landing list for doctor/clinic users.
@@ -22,7 +23,7 @@ export default function SettingsMenuPage() {
return (
<div className="fade-in" style={{ maxWidth: 720, margin: '0 auto' }}>
<h1 className="section-title" style={{ marginBottom: 16 }}>تنظیمات</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title"><h1 className="section-title" style={{ marginBottom: 16 }}>تنظیمات</h1><TourButton tourId="settings-menu" ready /></div>
{groups.map((group) => (
<section key={group.key} aria-label={group.label} style={{ marginBottom: 20 }}>
+1
View File
@@ -46,6 +46,7 @@ export default function SkillsPage() {
<div className="fade-in">
<PageHeader
title="مهارت‌ها"
tourId="resource-skills"
description="مهارت روی منابع می‌نشیند و در انتخاب منبع مناسب استفاده می‌شود. مهارتی که به منبعی داده شده، تا برداشته نشود حذف نمی‌شود."
backTo="/admin/resources"
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: 'مهارت‌ها' }]}
+2 -2
View File
@@ -117,10 +117,10 @@ function SmsWalletPageInner() {
return (
<>
<PageHeader title="کیف پول پیامک" description="مدیریت موجودی و تنظیمات ارسال پیامک" />
<PageHeader title="کیف پول پیامک" description="مدیریت موجودی و تنظیمات ارسال پیامک" tourId="sms-wallet" />
{/* ردیف بالا: موجودی + آمار */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16, marginBottom: 20 }}>
<div data-tour="sms-balance" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16, marginBottom: 20 }}>
{/* کارت موجودی — gradient */}
<div style={{
background: 'linear-gradient(135deg, oklch(0.52 0.22 256), oklch(0.40 0.18 256))',
+1
View File
@@ -189,6 +189,7 @@ export default function StaffPage() {
<SettingsLayout active="staff">
<PageHeader
title="مدیریت پرسنل"
tourId="staff"
description="لیست پرسنل کلینیک / مطب"
action={
canCreate ? (
@@ -102,6 +102,7 @@ export default function StaffSessionDetailPage() {
<>
<PageHeader
title={`جلسهٔ ${formatNumber(session.session_number)} از ${formatNumber(session.total_sessions)}`}
tourId="my-session-detail"
backTo="/admin/my-sessions"
/>
@@ -37,6 +37,7 @@ export default function StaffTreatmentSessionsPage() {
<>
<PageHeader
title="جلسات امروز من"
tourId="my-sessions"
action={
<button type="button" className="btn secondary sm" onClick={() => refetch()} disabled={isFetching}>
{isFetching ? 'در حال به‌روزرسانی…' : 'به‌روزرسانی'}
+7 -3
View File
@@ -10,6 +10,7 @@ import { usePaymentConfig } from '../hooks/usePaymentConfig';
import { formatRial, formatNumber, formatDate, formatResourceLimit } from '../lib/utils';
import Modal from '../components/ui/Modal';
import SettingsLayout from '../components/layout/SettingsLayout';
import TourButton from '../components/ui/TourButton';
import {
SparkleIcon, PlanCardPerson, CloseCircleFilled, GiftIcon, InfoCircleRedFilled,
} from './subscriptionIcons';
@@ -135,9 +136,12 @@ export default function SubscriptionPage() {
<div dir="ltr" style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 32, paddingTop: 8 }}>
{/* ── Header: current plan (left) + title (right) ── */}
<div style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center', gap: 16, flexWrap: 'wrap' }}>
{!myLoading && <CurrentPlanCard my={my} onRenew={renewCurrentPlan} />}
{!myLoading && <div data-tour="subscription-current"><CurrentPlanCard my={my} onRenew={renewCurrentPlan} /></div>}
<div>
<div style={{ fontSize: 22, fontWeight: 800, color: 'var(--text)' }}>انتخاب پلن اشتراک</div>
<div style={{ fontSize: 22, fontWeight: 800, color: 'var(--text)', display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title">
انتخاب پلن اشتراک
<TourButton tourId="subscription" ready={!plansLoading} />
</div>
<div style={{ fontSize: 14, fontWeight: 500, color: 'var(--text-3)', marginTop: 4, textAlign: 'left' }}>
پلن اشتراک مناسب خود را انتخاب نمایید:
</div>
@@ -156,7 +160,7 @@ export default function SubscriptionPage() {
در حال حاضر پلنی برای نمایش وجود ندارد.
</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 16, width: '100%' }}>
<div data-tour="subscription-plans" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 16, width: '100%' }}>
{plans.map((plan) => (
<PlanCard
key={plan.uuid}
+2 -1
View File
@@ -12,6 +12,7 @@ import ConfirmDialog from '../components/ui/ConfirmDialog';
import SettingsLayout from '../components/layout/SettingsLayout';
import { usePermissions } from '../hooks/usePermissions';
import Switch from '../components/ui/Switch';
import TourButton from '../components/ui/TourButton';
interface TenantTag { uuid: string; name: string; color: string; active: boolean }
@@ -72,7 +73,7 @@ export default function TagsSettingsPage() {
<SettingsLayout active="tags">
<div className="fade-in">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16, gap: 12, flexWrap: 'wrap' }}>
<h1 className="section-title">برچسبها</h1>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title"><h1 className="section-title">برچسبها</h1><TourButton tourId="tags-settings" ready /></div>
{canCreate && (
<button className="btn primary" onClick={openCreate}><PlusIcon style={{ width: 16 }} /> برچسب جدید</button>
)}
+2 -2
View File
@@ -58,9 +58,9 @@ export default function TreatmentCasesPage() {
return (
<>
<PageHeader title="دوره‌های درمان" />
<PageHeader title="دوره‌های درمان" tourId="treatment-cases" />
<div className="tabs" style={{ marginBottom: 16 }}>
<div className="tabs" data-tour="treatment-tabs" style={{ marginBottom: 16 }}>
{TABS.map((t) => (
<button
key={t.id}