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:
@@ -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 }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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={(
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: اطلاعات حساب ──────────────────────────────── */}
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -59,6 +59,7 @@ function MyFinancialPageContent() {
|
||||
<div className="page">
|
||||
<PageHeader
|
||||
title="مدیریت پرداخت"
|
||||
tourId="my-financial"
|
||||
description={`روشهای پرداخت «${environmentName}» (حساب بانکی و کارتخوان)`}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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={[
|
||||
|
||||
@@ -129,6 +129,7 @@ export default function MyPaymentsPage() {
|
||||
<>
|
||||
<PageHeader
|
||||
title="لیست پرداختها"
|
||||
tourId="my-payments"
|
||||
description="پرداختهای ثبتشدهی بیماران شما"
|
||||
breadcrumbs={[{ label: 'داشبورد', to: '/admin' }, { label: 'لیست پرداختها' }]}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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: 'استخر منابع' }]}
|
||||
|
||||
@@ -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: 'نوع منابع' }]}
|
||||
|
||||
@@ -104,6 +104,7 @@ export default function ResourcesPage() {
|
||||
<div className="fade-in">
|
||||
<PageHeader
|
||||
title="منابع"
|
||||
tourId="resources"
|
||||
description="هر چیزی که ممکن است اشغال باشد: پزشک، اپراتور، اتاق، دستگاه. ظرفیت یعنی تعداد بیمار همزمان."
|
||||
/* سقف پلن پیش از باز شدن فرم گفته میشود، نه بعد از پر کردنش: خطای ۴۲۲ ته کار
|
||||
همان اطلاعات را دیرتر و گرانتر میداد. تصمیم نهایی همچنان با سرور است. */
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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: 'مهارتها' }]}
|
||||
|
||||
@@ -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))',
|
||||
|
||||
@@ -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 ? 'در حال بهروزرسانی…' : 'بهروزرسانی'}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user