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:
@@ -63,9 +63,11 @@ export default function DataTable<T extends object>({
|
|||||||
<div>
|
<div>
|
||||||
{/* Toolbar row */}
|
{/* Toolbar row */}
|
||||||
{(onSearchChange !== undefined || headerExtra) && (
|
{(onSearchChange !== undefined || headerExtra) && (
|
||||||
<div className="toolbar">
|
// لنگرهای data-tour اینجا تعریف میشوند تا هر صفحهای که DataTable دارد
|
||||||
|
// بدون تغییر کد خودش، در تور راهنما قابل اشاره باشد.
|
||||||
|
<div className="toolbar" data-tour="page-toolbar">
|
||||||
{onSearchChange !== undefined && (
|
{onSearchChange !== undefined && (
|
||||||
<div className="field" style={{ flex: '0 0 auto', minWidth: 240 }}>
|
<div className="field" data-tour="page-search" style={{ flex: '0 0 auto', minWidth: 240 }}>
|
||||||
<MagnifyingGlassIcon style={{ width: 16, height: 16, flexShrink: 0 }} />
|
<MagnifyingGlassIcon style={{ width: 16, height: 16, flexShrink: 0 }} />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -79,7 +81,7 @@ export default function DataTable<T extends object>({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="table-wrap">
|
<div className="table-wrap" data-tour="page-table">
|
||||||
<table className="t">
|
<table className="t">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -21,9 +21,14 @@ interface Props {
|
|||||||
backTo?: string;
|
backTo?: string;
|
||||||
/** شناسهٔ تور راهنمای این صفحه؛ اگر در registry ثبت نشده باشد دکمهای نمیآید. */
|
/** شناسهٔ تور راهنمای این صفحه؛ اگر در registry ثبت نشده باشد دکمهای نمیآید. */
|
||||||
tourId?: string;
|
tourId?: string;
|
||||||
|
/**
|
||||||
|
* محتوای صفحه نشسته است و تورِ بار اول میتواند خودکار اجرا شود.
|
||||||
|
* صفحهای که دادهاش دیر میآید، `!isLoading` بدهد تا تور روی صفحهٔ نیمهساخته نیفتد.
|
||||||
|
*/
|
||||||
|
tourReady?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PageHeader({ title, breadcrumbs, action, description, backTo, tourId }: Props) {
|
export default function PageHeader({ title, breadcrumbs, action, description, backTo, tourId, tourReady = true }: Props) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, marginBottom: 24 }}>
|
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, marginBottom: 24 }}>
|
||||||
<div style={{ minWidth: 0 }}>
|
<div style={{ minWidth: 0 }}>
|
||||||
@@ -51,15 +56,15 @@ export default function PageHeader({ title, breadcrumbs, action, description, ba
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }} data-tour="page-title">
|
||||||
<h1 className="section-title">{title}</h1>
|
<h1 className="section-title">{title}</h1>
|
||||||
<TourButton tourId={tourId} />
|
<TourButton tourId={tourId} ready={tourReady} />
|
||||||
</div>
|
</div>
|
||||||
{description && (
|
{description && (
|
||||||
<p style={{ fontSize: 13, color: 'var(--text-3)', marginTop: 4 }}>{description}</p>
|
<p style={{ fontSize: 13, color: 'var(--text-3)', marginTop: 4 }}>{description}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{action && <div style={{ flexShrink: 0 }}>{action}</div>}
|
{action && <div style={{ flexShrink: 0 }} data-tour="page-action">{action}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function Pagination({ page, total, limit, onPageChange }: Props)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pagination">
|
<div className="pagination" data-tour="page-pagination">
|
||||||
<span style={{ fontSize: 12.5, color: 'var(--text-3)' }}>
|
<span style={{ fontSize: 12.5, color: 'var(--text-3)' }}>
|
||||||
نمایش {formatNumber(from)}–{formatNumber(to)} از {formatNumber(total)} مورد
|
نمایش {formatNumber(from)}–{formatNumber(to)} از {formatNumber(total)} مورد
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -8,17 +8,21 @@ import { getTour } from '../../lib/tour/registry';
|
|||||||
* وجود تور قبل از هر هوکی بررسی میشود تا صفحهای که راهنما ندارد — یعنی بیشتر
|
* وجود تور قبل از هر هوکی بررسی میشود تا صفحهای که راهنما ندارد — یعنی بیشتر
|
||||||
* صفحات پنل — هیچ درخواستی برای وضعیت تورها نفرستد.
|
* صفحات پنل — هیچ درخواستی برای وضعیت تورها نفرستد.
|
||||||
*/
|
*/
|
||||||
export default function TourButton({ tourId }: { tourId?: string }) {
|
export default function TourButton({ tourId, ready = false }: { tourId?: string; ready?: boolean }) {
|
||||||
const tour = getTour(tourId);
|
const tour = getTour(tourId);
|
||||||
|
|
||||||
if (!tour) return null;
|
if (!tour) return null;
|
||||||
|
|
||||||
return <TourLauncher tourId={tour.id} />;
|
return <TourLauncher tourId={tour.id} ready={ready} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** اجرای خودکار وظیفهٔ خود صفحه است؛ این دکمه فقط دستی اجرا میکند. */
|
/**
|
||||||
function TourLauncher({ tourId }: { tourId: string }) {
|
* `ready` یعنی محتوای صفحه نشسته است و اجرای خودکارِ بار اول مجاز است.
|
||||||
const { start } = useTour(tourId);
|
* صفحهای که دکمه را مستقیم میگذارد و آمادهبودنش را نمیداند، آن را false میگذارد
|
||||||
|
* و تور فقط با کلیک اجرا میشود.
|
||||||
|
*/
|
||||||
|
function TourLauncher({ tourId, ready }: { tourId: string; ready: boolean }) {
|
||||||
|
const { start } = useTour(tourId, { ready });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,10 +1,97 @@
|
|||||||
import type { TourDefinition } from './types';
|
import type { TourDefinition } from './types';
|
||||||
import { appointmentsTour } from './tours/appointments';
|
import { appointmentsTour } from './tours/appointments';
|
||||||
|
import { appointmentDetailTour, reserveAppointmentsTour } from './tours/appointmentsMore';
|
||||||
|
import { appointmentCreateTour, appointmentEditTour, patientDetailTour } from './tours/patientDetail';
|
||||||
|
import { myPatientRecordTour, myPatientsTour } from './tours/myPatients';
|
||||||
|
import { patientsTour } from './tours/patients';
|
||||||
|
import {
|
||||||
|
claimDetailTour, claimsTour, discountsTour, insurancePricingTour, myFinancialTour,
|
||||||
|
myPaymentDetailTour, myPaymentsTour, smsWalletTour, subscriptionTour,
|
||||||
|
} from './tours/financial';
|
||||||
|
import {
|
||||||
|
clinicServicesTour, inventoryTour, resourceDetailTour, resourcePoolsTour, resourceSkillsTour,
|
||||||
|
resourceTypesTour, resourcesTour, serviceCategoriesTour, serviceDetailTour, treatmentCasesTour,
|
||||||
|
} from './tours/services';
|
||||||
|
import {
|
||||||
|
accountSettingsTour, appointmentSettingsTour, clinicAppointmentSettingsTour, clinicDoctorsTour,
|
||||||
|
holidaysTour, practiceDomainTour, recordNumberSettingsTour, settingsMenuTour, tagsSettingsTour,
|
||||||
|
} from './tours/settings';
|
||||||
|
import {
|
||||||
|
clinicDetailTour, clinicsTour, dashboardTour, doctorDetailTour, doctorFormTour, doctorsTour,
|
||||||
|
mySecretariesTour, secretaryEarningsTour, secretarySettlementTour, staffSessionDetailTour,
|
||||||
|
staffSessionsTour, staffTour,
|
||||||
|
} from './tours/clinicStaff';
|
||||||
|
import {
|
||||||
|
patientRecordFormTour, representationBlogFormTour, representationBlogsTour,
|
||||||
|
representationFinanceTour, representationProfileTour, representationSettlementTour,
|
||||||
|
sessionEditTour, sessionNewTour, sessionPaymentTour,
|
||||||
|
} from './tours/sessionsAndRepresentation';
|
||||||
|
|
||||||
/** هر صفحه تور خودش را در tours/ دارد و فقط اینجا ثبت میشود. */
|
/** هر صفحه تور خودش را در tours/ دارد و فقط اینجا ثبت میشود. */
|
||||||
export const TOURS: Record<string, TourDefinition> = {
|
const ALL: TourDefinition[] = [
|
||||||
[appointmentsTour.id]: appointmentsTour,
|
appointmentsTour,
|
||||||
};
|
appointmentDetailTour,
|
||||||
|
reserveAppointmentsTour,
|
||||||
|
appointmentCreateTour,
|
||||||
|
appointmentEditTour,
|
||||||
|
patientsTour,
|
||||||
|
patientDetailTour,
|
||||||
|
myPatientsTour,
|
||||||
|
myPatientRecordTour,
|
||||||
|
myPaymentsTour,
|
||||||
|
myPaymentDetailTour,
|
||||||
|
myFinancialTour,
|
||||||
|
claimsTour,
|
||||||
|
claimDetailTour,
|
||||||
|
insurancePricingTour,
|
||||||
|
discountsTour,
|
||||||
|
subscriptionTour,
|
||||||
|
smsWalletTour,
|
||||||
|
clinicServicesTour,
|
||||||
|
serviceDetailTour,
|
||||||
|
serviceCategoriesTour,
|
||||||
|
resourcesTour,
|
||||||
|
resourceTypesTour,
|
||||||
|
resourcePoolsTour,
|
||||||
|
resourceSkillsTour,
|
||||||
|
resourceDetailTour,
|
||||||
|
inventoryTour,
|
||||||
|
treatmentCasesTour,
|
||||||
|
settingsMenuTour,
|
||||||
|
accountSettingsTour,
|
||||||
|
tagsSettingsTour,
|
||||||
|
recordNumberSettingsTour,
|
||||||
|
appointmentSettingsTour,
|
||||||
|
clinicAppointmentSettingsTour,
|
||||||
|
holidaysTour,
|
||||||
|
practiceDomainTour,
|
||||||
|
clinicDoctorsTour,
|
||||||
|
dashboardTour,
|
||||||
|
clinicsTour,
|
||||||
|
clinicDetailTour,
|
||||||
|
doctorsTour,
|
||||||
|
doctorDetailTour,
|
||||||
|
doctorFormTour,
|
||||||
|
mySecretariesTour,
|
||||||
|
staffTour,
|
||||||
|
staffSessionsTour,
|
||||||
|
staffSessionDetailTour,
|
||||||
|
secretaryEarningsTour,
|
||||||
|
secretarySettlementTour,
|
||||||
|
sessionNewTour,
|
||||||
|
sessionEditTour,
|
||||||
|
sessionPaymentTour,
|
||||||
|
patientRecordFormTour,
|
||||||
|
representationBlogsTour,
|
||||||
|
representationBlogFormTour,
|
||||||
|
representationFinanceTour,
|
||||||
|
representationProfileTour,
|
||||||
|
representationSettlementTour,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TOURS: Record<string, TourDefinition> = Object.fromEntries(
|
||||||
|
ALL.map((tour) => [tour.id, tour]),
|
||||||
|
);
|
||||||
|
|
||||||
export function getTour(id?: string): TourDefinition | null {
|
export function getTour(id?: string): TourDefinition | null {
|
||||||
return id ? TOURS[id] ?? null : null;
|
return id ? TOURS[id] ?? null : null;
|
||||||
|
|||||||
@@ -59,4 +59,50 @@ describe('registry', () => {
|
|||||||
expect(new Set(anchors).size).toBe(anchors.length);
|
expect(new Set(anchors).size).toBe(anchors.length);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('متن هر استپ فارسی و غیرخالی است', () => {
|
||||||
|
for (const tour of Object.values(TOURS)) {
|
||||||
|
for (const step of tour.steps) {
|
||||||
|
expect(step.title.trim().length).toBeGreaterThan(2);
|
||||||
|
expect(step.body.trim().length).toBeGreaterThan(10);
|
||||||
|
// متن راهنما برای کاربر فارسیزبان است؛ استپِ انگلیسیمانده یعنی جا افتاده.
|
||||||
|
expect(step.title).toMatch(/[-ۿ]/);
|
||||||
|
expect(step.body).toMatch(/[-ۿ]/);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* لنگرِ بیالمان بیصدا حذف میشود، پس تورِ خراب هیچ خطایی نمیدهد و فقط استپش را
|
||||||
|
* از دست میدهد. این تست همان سکوت را میشکند: هر anchor باید در کدِ صفحات وجود
|
||||||
|
* داشته باشد.
|
||||||
|
*/
|
||||||
|
describe('انطباق لنگرها با کد صفحات', () => {
|
||||||
|
it('هیچ استپی به data-tour ناموجود اشاره نمیکند', async () => {
|
||||||
|
// خواندن مستقیم فایلها، نه import.meta.glob: آن فقط زیر Vite کار میکند و
|
||||||
|
// ts-loaderِ Encore همین فایل را هم type-check میکند و build را قرمز میکرد.
|
||||||
|
const { readdirSync, readFileSync } = await import('node:fs');
|
||||||
|
const { join } = await import('node:path');
|
||||||
|
|
||||||
|
const walk = (dir: string): string[] =>
|
||||||
|
readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||||
|
const path = join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) return walk(path);
|
||||||
|
return entry.name.endsWith('.tsx') && !entry.name.includes('.test.') ? [path] : [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const sources = walk('assets/admin').map((path) => readFileSync(path, 'utf8'));
|
||||||
|
const declared = new Set(
|
||||||
|
sources.flatMap((src) => [...src.matchAll(/data-tour="([a-z0-9-]+)"/g)].map((m) => m[1])),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(declared.size).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const orphans = Object.values(TOURS).flatMap((tour) =>
|
||||||
|
tour.steps.filter((s) => !declared.has(s.anchor)).map((s) => `${tour.id}:${s.anchor}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(orphans).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import type { TourDefinition } from '../types';
|
||||||
|
|
||||||
|
export const appointmentDetailTour: TourDefinition = {
|
||||||
|
id: 'appointment-detail',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
anchor: 'appt-patient',
|
||||||
|
title: 'اطلاعات نوبت',
|
||||||
|
body: 'نام بیمار، پزشک، خدمت، ساعت و هزینهٔ همین نوبت.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'appt-status',
|
||||||
|
title: 'وضعیت و اقدامات',
|
||||||
|
body: 'وضعیت فعلی نوبت را از اینجا عوض کنید — مثلاً تأیید، حضور بیمار یا لغو.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'appt-events',
|
||||||
|
title: 'تاریخچهٔ رویدادها',
|
||||||
|
body: 'هر تغییر وضعیت با زمان و عاملش ثبت میشود؛ برای پیگیری اختلاف مفید است.',
|
||||||
|
side: 'top',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reserveAppointmentsTour: TourDefinition = {
|
||||||
|
id: 'appointments-reserve',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
anchor: 'page-title',
|
||||||
|
title: 'نوبتهای رزرو شده',
|
||||||
|
body: 'نوبتهایی که بیرون از تقویم عادی و بهصورت دستی رزرو شدهاند.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'reserve-doctor',
|
||||||
|
title: 'انتخاب پزشک',
|
||||||
|
body: 'در کلینیک، اول پزشک را انتخاب کنید تا نوبتهای رزروش بیاید.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'reserve-new',
|
||||||
|
title: 'نوبت رزرو جدید',
|
||||||
|
body: 'تا وقتی پزشکی انتخاب نشده، این دکمه غیرفعال است.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'reserve-list',
|
||||||
|
title: 'فهرست رزروها',
|
||||||
|
body: 'هر ردیف یک نوبت رزروشده با بیمار و زمان آن.',
|
||||||
|
side: 'top',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import type { TourDefinition } from '../types';
|
||||||
|
|
||||||
|
export const dashboardTour: TourDefinition = {
|
||||||
|
id: 'dashboard',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'داشبورد', body: 'خلاصهٔ امروز: نوبتها، درآمد، بیماران و کارهای در انتظار.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clinicsTour: TourDefinition = {
|
||||||
|
id: 'clinics',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'کلینیکها', body: 'کلینیکهایی که به آنها دسترسی دارید.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-pagination', title: 'صفحهبندی', body: 'تعداد کل و جابهجایی بین صفحهها.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clinicDetailTour: TourDefinition = {
|
||||||
|
id: 'clinic-detail',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'پروندهٔ کلینیک', body: 'اطلاعات، آدرس، تصاویر و تنظیمات این کلینیک.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doctorsTour: TourDefinition = {
|
||||||
|
id: 'doctors',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'پزشکان', body: 'فهرست پزشکان و دسترسی به پروفایل هرکدام.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-pagination', title: 'صفحهبندی', body: 'تعداد کل پزشکان و حرکت بین صفحهها.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doctorDetailTour: TourDefinition = {
|
||||||
|
id: 'doctor-detail',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'پروفایل پزشک', body: 'مشخصات، تخصصها، آدرسها، تعرفهها و وضعیت فعالبودن پزشک.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doctorFormTour: TourDefinition = {
|
||||||
|
id: 'doctor-new',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'افزودن پزشک', body: 'پزشک جدید را با موبایل و تخصصش ثبت کنید.', side: 'bottom' },
|
||||||
|
{ anchor: 'doctor-form', title: 'فرم', body: 'موبایل کلید حساب است؛ اگر کاربری با آن شماره باشد، به همان وصل میشود.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mySecretariesTour: TourDefinition = {
|
||||||
|
id: 'my-secretaries',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'منشیها', body: 'منشیهای شما و دسترسیهایشان.', side: 'bottom' },
|
||||||
|
{ anchor: 'secretaries-tabs', title: 'فعلی و قبلی', body: 'منشی قطعهمکاریشده در تب دوم میماند و حذف نمیشود.', side: 'bottom' },
|
||||||
|
{ anchor: 'secretaries-add', title: 'افزودن منشی', body: 'بعد از ذخیره، پنجرهٔ مجوزها باز میشود تا دسترسیهایش را دقیق تعیین کنید. سقف تعداد به پلن اشتراک بستگی دارد.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const staffTour: TourDefinition = {
|
||||||
|
id: 'staff',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'مدیریت پرسنل', body: 'کارکنان درمانی که جلسه انجام میدهند — با مهارتها و سهمشان.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-table', title: 'فهرست پرسنل', body: 'مهارت هر نفر تعیین میکند در چه سرویسی قابل انتخاب است.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const staffSessionsTour: TourDefinition = {
|
||||||
|
id: 'my-sessions',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'جلسات امروز من', body: 'جلسههایی که امروز به شما سپرده شده است.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const staffSessionDetailTour: TourDefinition = {
|
||||||
|
id: 'my-session-detail',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'جزئیات جلسه', body: 'خدمت، بیمار، مصرف انبار و ثبت انجام جلسه.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const secretaryEarningsTour: TourDefinition = {
|
||||||
|
id: 'secretary-earnings',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'درآمد نوبتهای آنلاین', body: 'سهم شما از نوبتهایی که آنلاین ثبت و پرداخت شدهاند.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-table', title: 'ریز نوبتها', body: 'هر ردیف یک نوبت با سهم شما از آن.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const secretarySettlementTour: TourDefinition = {
|
||||||
|
id: 'secretary-settlement',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'تسویه حساب', body: 'برداشت سهم نوبتهای آنلاین به شماره شبای خودتان.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-table', title: 'درخواستها', body: 'وضعیت هر درخواست برداشت اینجا دیده میشود.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import type { TourDefinition } from '../types';
|
||||||
|
|
||||||
|
export const myPaymentsTour: TourDefinition = {
|
||||||
|
id: 'my-payments',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'لیست پرداختها', body: 'همهٔ پرداختهای ثبتشدهٔ بیماران شما، جدیدترین بالا.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-search', title: 'جستجو', body: 'با نام بیمار یا شمارهٔ صورتحساب بگردید.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-table', title: 'ردیفهای پرداخت', body: 'با کلیک روی هر بیمار، صورتحسابها و آیتمهایش باز میشود.', side: 'top' },
|
||||||
|
{ anchor: 'page-pagination', title: 'صفحهبندی', body: 'تعداد کل و جابهجایی بین صفحهها.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const myPaymentDetailTour: TourDefinition = {
|
||||||
|
id: 'my-payment-detail',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'پرداختهای این بیمار', body: 'صورتحسابهای ثبتشده برای همین بیمار و ماندهحسابش.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-table', title: 'آیتمهای صورتحساب', body: 'هر ردیف یک خدمت با سهم بیمار و سهم بیمه.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const myFinancialTour: TourDefinition = {
|
||||||
|
id: 'my-financial',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'مدیریت پرداخت', body: 'درگاه، حساب بانکی و روشهای دریافت وجه از بیمار.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const claimsTour: TourDefinition = {
|
||||||
|
id: 'claims',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'پروندههای بیمه', body: 'مطالبات بیمه به تفکیک بیمار؛ چقدر ثبت شده، چقدر وصول شده.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-search', title: 'جستجوی بیمار', body: 'با نام یا کد ملی بیمار پیدایش کنید.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-table', title: 'فهرست مطالبات', body: 'روی هر ردیف بزنید تا ریز خدمات و سهم بیمهاش باز شود.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const claimDetailTour: TourDefinition = {
|
||||||
|
id: 'claim-detail',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'جزئیات مطالبه', body: 'خدمات بیمهایِ همین بیمار و وضعیت وصول هرکدام.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-table', title: 'ریز خدمات', body: 'سهم بیمه و سهم بیمار برای هر خدمت جدا آمده است.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insurancePricingTour: TourDefinition = {
|
||||||
|
id: 'insurance-pricing',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'مدیریت بیمه', body: 'قراردادهای بیمهٔ پایه و تکمیلی و درصد پوشش هرکدام.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const discountsTour: TourDefinition = {
|
||||||
|
id: 'discounts',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'مدیریت تخفیفها', body: 'قوانین تخفیف که هنگام صدور صورتحساب اعمال میشوند.', side: 'bottom' },
|
||||||
|
{ anchor: 'discounts-rules', title: 'قوانین', body: 'برای هر خدمت یا کل فاکتور، درصد یا مبلغ ثابت تعریف کنید.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subscriptionTour: TourDefinition = {
|
||||||
|
id: 'subscription',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'اشتراک', body: 'پلن فعلی و پلنهای قابل خرید.', side: 'bottom' },
|
||||||
|
{ anchor: 'subscription-current', title: 'پلن فعلی', body: 'روزهای باقیمانده و دکمهٔ تمدید همینجاست.', side: 'bottom' },
|
||||||
|
{ anchor: 'subscription-plans', title: 'پلنها', body: 'امکانات هر پلن زیرش نوشته شده؛ خرید از روی همین کارتها انجام میشود.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const smsWalletTour: TourDefinition = {
|
||||||
|
id: 'sms-wallet',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'کیف پول پیامک', body: 'ارسال پیامک به بیماران از موجودی همین کیف پول کم میشود.', side: 'bottom' },
|
||||||
|
{ anchor: 'sms-balance', title: 'موجودی و مصرف', body: 'موجودی فعلی و آمار مصرف. وقتی موجودی کم شود، اینجا هشدار میگیرید.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-pagination', title: 'تاریخچه', body: 'شارژها و مصرفهای قبلی صفحهبندی شدهاند.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import type { TourDefinition } from '../types';
|
||||||
|
|
||||||
|
export const myPatientsTour: TourDefinition = {
|
||||||
|
id: 'my-patients',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
anchor: 'page-title',
|
||||||
|
title: 'پروندهٔ بیماران',
|
||||||
|
body: 'فهرست مراجعهکنندگانی که برایشان پرونده ثبت شده است. با ثبت اولین نوبت، پرونده خودکار ساخته میشود.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'page-action',
|
||||||
|
title: 'پروندهٔ دستی',
|
||||||
|
body: 'برای بیماری که هنوز نوبت نگرفته، از اینجا پرونده بسازید.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'patients-search',
|
||||||
|
title: 'جستجو و نمای فهرست',
|
||||||
|
body: 'جستجو با نام، موبایل یا کد ملی. دو آیکون کنارش، نمای کارتی و لیستی را عوض میکنند.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'patients-records',
|
||||||
|
title: 'پروندهها',
|
||||||
|
body: 'با کلیک روی هر پرونده، اطلاعات بیمار و مراجعهها و پرداختهایش باز میشود.',
|
||||||
|
side: 'top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'page-pagination',
|
||||||
|
title: 'صفحهبندی',
|
||||||
|
body: 'شمار کل پروندهها و جابهجایی بین صفحهها.',
|
||||||
|
side: 'top',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const myPatientRecordTour: TourDefinition = {
|
||||||
|
id: 'my-patient-record',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
anchor: 'record-banner',
|
||||||
|
title: 'شناسنامهٔ بیمار',
|
||||||
|
body: 'خلاصهٔ هویتی و تماس بیمار، شمارهٔ پرونده و تاریخ تشکیل آن.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'record-tabs',
|
||||||
|
title: 'بخشهای پرونده',
|
||||||
|
body: 'اطلاعات پرونده، مراجعهها، پرداختها و نوبتها؛ هر کدام در تب خودش.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'page-action',
|
||||||
|
title: 'مراجعهٔ جدید و بازگشت',
|
||||||
|
body: 'ثبت مراجعهٔ تازه برای همین بیمار، یا برگشتن به فهرست پروندهها.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import type { TourDefinition } from '../types';
|
||||||
|
|
||||||
|
export const patientDetailTour: TourDefinition = {
|
||||||
|
id: 'patient-detail',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
anchor: 'patient-banner',
|
||||||
|
title: 'شناسنامهٔ پرونده',
|
||||||
|
body: 'نام، شمارهٔ پرونده، برچسبها، نوبت و جلسهٔ بعدی و وضعیت بدهی — همه یکجا.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'patient-tabs',
|
||||||
|
title: 'بخشهای پرونده',
|
||||||
|
body: 'سرویسها، نوبتها، دورههای درمان، پرداختها، کیف پول، یادداشت، ضمیمه و پروندهٔ پزشکی.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appointmentCreateTour: TourDefinition = {
|
||||||
|
id: 'appointment-create',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
anchor: 'page-title',
|
||||||
|
title: 'ثبت نوبت جدید',
|
||||||
|
body: 'چهار قدم دارد: پزشک، مراجعهکننده، سرویس و زمان.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'create-doctor',
|
||||||
|
title: 'پزشک یا منبع',
|
||||||
|
body: 'اول پزشک را انتخاب کنید؛ سرویسها و ساعتهای آزاد از برنامهٔ همو میآیند.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'create-patient',
|
||||||
|
title: 'مراجعهکننده',
|
||||||
|
body: 'بیمار قبلی را جستجو کنید، یا مشخصات شخص جدید را وارد کنید تا پروندهاش ساخته شود.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'create-service',
|
||||||
|
title: 'سرویسها',
|
||||||
|
body: 'اول بخش، بعد یک یا چند سرویس. مدت و هزینهٔ نوبت از همینها حساب میشود.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'create-submit',
|
||||||
|
title: 'ثبت',
|
||||||
|
body: 'تا وقتی پزشک، بیمار، سرویس و زمان کامل نشده، دکمه غیرفعال است.',
|
||||||
|
side: 'top',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appointmentEditTour: TourDefinition = {
|
||||||
|
id: 'appointment-edit',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
anchor: 'edit-service',
|
||||||
|
title: 'ویرایش نوبت',
|
||||||
|
body: 'بخش، سرویس، پرسنل، زمان و وضعیت نوبت را از همینجا عوض کنید.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { TourDefinition } from '../types';
|
||||||
|
|
||||||
|
export const patientsTour: TourDefinition = {
|
||||||
|
id: 'patients',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
anchor: 'page-title',
|
||||||
|
title: 'پروندهها',
|
||||||
|
body: 'فهرست کامل پروندههای بیماران این محیط، با امکان جستجو و فیلتر.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'patients-search',
|
||||||
|
title: 'جستجو',
|
||||||
|
body: 'کد ملی، شمارهٔ پرونده یا نام مراجعهکننده را بنویسید.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'patients-filters',
|
||||||
|
title: 'فیلترها',
|
||||||
|
body: 'جنسیت، بیمه، بازهٔ پذیرش، بدهی و برچسب. عدد روی دکمه یعنی چند فیلتر فعال است.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'patients-new',
|
||||||
|
title: 'تشکیل پرونده',
|
||||||
|
body: 'پروندهٔ جدید برای بیماری که هنوز ثبت نشده است.',
|
||||||
|
side: 'bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: 'page-pagination',
|
||||||
|
title: 'صفحهبندی',
|
||||||
|
body: 'نمای کارتی ۱۶ و نمای جدولی ۱۲ پرونده در هر صفحه نشان میدهد.',
|
||||||
|
side: 'top',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import type { TourDefinition } from '../types';
|
||||||
|
|
||||||
|
export const clinicServicesTour: TourDefinition = {
|
||||||
|
id: 'clinic-services',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'سرویسها', body: 'خدماتی که در نوبتدهی و صورتحساب استفاده میشوند؛ هرکدام مدت و تعرفهٔ خودش را دارد.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-action', title: 'سرویس جدید', body: 'خدمت تازه را اینجا تعریف کنید تا در ثبت نوبت قابل انتخاب شود.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serviceDetailTour: TourDefinition = {
|
||||||
|
id: 'service-detail',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'جزئیات سرویس', body: 'تعرفه، مدت، بیمه و تنظیمات نوبتدهی همین خدمت.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serviceCategoriesTour: TourDefinition = {
|
||||||
|
id: 'service-categories',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'دستهبندیها', body: 'سرویسها را دستهبندی کنید تا در فهرستها و فیلترها مرتب بمانند.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-table', title: 'دستهها', body: 'هر دسته را میتوان ویرایش یا حذف کرد.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resourcesTour: TourDefinition = {
|
||||||
|
id: 'resources',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'منابع', body: 'اتاق، تخت یا دستگاه — هر چیزی که خودش تقویم نوبت دارد.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-action', title: 'منبع جدید', body: 'منبع تازه بسازید و پزشک ناظرش را مشخص کنید.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-table', title: 'فهرست منابع', body: 'با کلیک روی هر منبع، تقویم و سرویسهایش باز میشود.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resourceTypesTour: TourDefinition = {
|
||||||
|
id: 'resource-types',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'نوع منابع', body: 'دستهبندی منابع؛ مثلاً اتاق عمل، یونیت دندانپزشکی یا دستگاه لیزر.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-table', title: 'نوعها', body: 'هنگام ساخت منبع، یکی از همین نوعها انتخاب میشود.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resourcePoolsTour: TourDefinition = {
|
||||||
|
id: 'resource-pools',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'استخر منابع', body: 'چند منبع همکار را در یک استخر بگذارید تا سیستم خودش یکی از آزادها را بدهد.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-table', title: 'استخرها', body: 'اعضای هر استخر از همینجا مدیریت میشوند.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resourceSkillsTour: TourDefinition = {
|
||||||
|
id: 'resource-skills',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'مهارتها', body: 'مهارت لازم برای هر خدمت؛ فقط منبع یا پرسنلِ دارای آن مهارت قابل انتخاب میشود.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-table', title: 'فهرست مهارتها', body: 'مهارتها را بسازید و بعد به منابع و پرسنل نسبت دهید.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resourceDetailTour: TourDefinition = {
|
||||||
|
id: 'resource-detail',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'جزئیات منبع', body: 'مشخصات منبع، سرویسهایش، مهارتها و دسترسی به تقویمش.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inventoryTour: TourDefinition = {
|
||||||
|
id: 'inventory',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'انبارداری', body: 'کالاهای مصرفی و پکیجها؛ مصرف هر جلسه از همین موجودی کم میشود.', side: 'bottom' },
|
||||||
|
{ anchor: 'inventory-tabs', title: 'دو بخش', body: 'کالای مصرفی و پکیج. پکیج چند کالا را با هم مصرف میکند.', side: 'bottom' },
|
||||||
|
{ anchor: 'inventory-stats', title: 'وضعیت انبار', body: 'ارزش موجودی و کالاهایی که به مرز هشدار رسیدهاند.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const treatmentCasesTour: TourDefinition = {
|
||||||
|
id: 'treatment-cases',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'دورههای درمان', body: 'درمانهای چندجلسهای بیماران و پیشرفت هر دوره.', side: 'bottom' },
|
||||||
|
{ anchor: 'treatment-tabs', title: 'بخشها', body: 'دورهها و جلسهها را جدا ببینید و بر اساس وضعیت فیلتر کنید.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import type { TourDefinition } from '../types';
|
||||||
|
|
||||||
|
export const sessionNewTour: TourDefinition = {
|
||||||
|
id: 'session-new',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'ثبت مراجعه', body: 'یک مراجعهٔ تازه برای همین بیمار ثبت میکنید.', side: 'bottom' },
|
||||||
|
{ anchor: 'session-card', title: 'فرم مراجعه', body: 'خدمت انجامشده، پرسنل و مبلغ. بعد از ثبت، صورتحسابش قابل صدور است.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sessionEditTour: TourDefinition = {
|
||||||
|
id: 'session-edit',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'ویرایش مراجعه', body: 'خدمت، پرسنل یا مبلغ همین مراجعه را اصلاح کنید.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sessionPaymentTour: TourDefinition = {
|
||||||
|
id: 'session-payment',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'پرداخت مراجعه', body: 'دریافت وجه بابت این مراجعه و ثبت روش پرداخت.', side: 'bottom' },
|
||||||
|
{ anchor: 'session-card', title: 'مبلغ و روش', body: 'سهم بیمه از سهم بیمار جدا حساب میشود؛ باقیمانده همان چیزی است که دریافت میکنید.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const patientRecordFormTour: TourDefinition = {
|
||||||
|
id: 'patient-record-form',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'پروندهٔ بیمار', body: 'ساخت یا ویرایش پروندهٔ یک مراجعهکننده.', side: 'bottom' },
|
||||||
|
{ anchor: 'record-form', title: 'فرم', body: 'موبایل کلید شناسایی بیمار است؛ کد ملی و بیمه بعداً هم قابل تکمیلاند.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const representationBlogsTour: TourDefinition = {
|
||||||
|
id: 'representation-blogs',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'وبلاگ من', body: 'مقالههایی که نوشتهاید و وضعیت بررسی هرکدام.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-action', title: 'مقالهٔ جدید', body: 'مقاله بعد از تأیید ادمین روی سایت منتشر میشود.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-table', title: 'فهرست مقالهها', body: 'وضعیت هر مقاله: پیشنویس، در انتظار بررسی یا منتشرشده.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const representationBlogFormTour: TourDefinition = {
|
||||||
|
id: 'representation-blog-form',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'نوشتن مقاله', body: 'عنوان، متن، تصویر و اطلاعات سئو مقاله.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const representationFinanceTour: TourDefinition = {
|
||||||
|
id: 'representation-finance',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'گزارش مالی', body: 'پورسانت شما از اشتراکهایی که مجموعههای تحت پوششتان خریدهاند.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-pagination', title: 'دورهها', body: 'رکوردهای مالی صفحهبندی شدهاند.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const representationProfileTour: TourDefinition = {
|
||||||
|
id: 'representation-profile',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'پروفایل نماینده', body: 'مشخصات شما، شهر تحت پوشش و اطلاعات حساب بانکی برای تسویه.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const representationSettlementTour: TourDefinition = {
|
||||||
|
id: 'representation-settlement',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'تسویه حساب', body: 'درخواست برداشت پورسانت و وضعیت درخواستهای قبلی.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-pagination', title: 'تاریخچه', body: 'درخواستهای قبلی و نتیجهٔ هرکدام.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import type { TourDefinition } from '../types';
|
||||||
|
|
||||||
|
export const settingsMenuTour: TourDefinition = {
|
||||||
|
id: 'settings-menu',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'تنظیمات', body: 'همهٔ تنظیمات محیط از همینجا شاخه میگیرد: نوبتدهی، سرویسها، بیمه، برچسبها، اشتراک و کاربران.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const accountSettingsTour: TourDefinition = {
|
||||||
|
id: 'account-settings',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'حساب کاربری', body: 'شمارهٔ ورود، رمز عبور و اطلاعات شخصی حسابتان.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tagsSettingsTour: TourDefinition = {
|
||||||
|
id: 'tags-settings',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'برچسبها', body: 'برچسبهایی که به پروندهٔ بیماران میچسبانید و بعد با آنها فیلتر میکنید.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const recordNumberSettingsTour: TourDefinition = {
|
||||||
|
id: 'record-number-settings',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'شمارهٔ پرونده', body: 'الگوی شمارهگذاری پروندههای جدید — پیشوند، طول و شمارهٔ شروع.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appointmentSettingsTour: TourDefinition = {
|
||||||
|
id: 'appointment-settings',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'مدیریت نوبتدهی', body: 'برنامهٔ هفتگی، مدت نوبت، تعطیلیها و قواعد لغو نوبت.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clinicAppointmentSettingsTour: TourDefinition = {
|
||||||
|
id: 'clinic-appointment-settings',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'نوبتدهی کلینیک', body: 'تنظیمات نوبتدهی در سطح کلینیک؛ روی همهٔ پزشکان عضو اثر میگذارد.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const holidaysTour: TourDefinition = {
|
||||||
|
id: 'holidays',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'تعطیلات', body: 'روزهایی که نوبتدهی بسته است. تعطیلات رسمی کشور از قبل هست و میتوانید روز دلخواه هم اضافه کنید.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-table', title: 'فهرست روزها', body: 'هر ردیف یک روز تعطیل؛ حذفش نوبتدهی همان روز را باز میکند.', side: 'top' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const practiceDomainTour: TourDefinition = {
|
||||||
|
id: 'practice-domain',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'حوزهٔ فعالیت', body: 'نوع کار محیط شما؛ فرمهای پرونده و گردش درمان بر اساس همین انتخاب میشود.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clinicDoctorsTour: TourDefinition = {
|
||||||
|
id: 'clinic-doctors',
|
||||||
|
version: 1,
|
||||||
|
steps: [
|
||||||
|
{ anchor: 'page-title', title: 'پزشکان کلینیک', body: 'پزشکان عضو، دعوت پزشک جدید و قطع همکاری.', side: 'bottom' },
|
||||||
|
{ anchor: 'page-action', title: 'دعوت پزشک', body: 'با شمارهٔ موبایل دعوت بفرستید؛ پزشک بعد از پذیرش، در نوبتدهی کلینیک دیده میشود.', side: 'bottom' },
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ import { toast } from 'sonner';
|
|||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
import SettingsLayout from '../components/layout/SettingsLayout';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
const ROLE_LABELS: Record<string, string> = {
|
const ROLE_LABELS: Record<string, string> = {
|
||||||
admin: 'مدیر', clinic: 'کلینیک', doctor: 'پزشک',
|
admin: 'مدیر', clinic: 'کلینیک', doctor: 'پزشک',
|
||||||
@@ -48,7 +49,7 @@ export default function AccountSettingsPage() {
|
|||||||
return (
|
return (
|
||||||
<SettingsLayout active="account">
|
<SettingsLayout active="account">
|
||||||
<div className="fade-in" style={{ display: 'flex', flexDirection: 'column', gap: 20, maxWidth: 620 }}>
|
<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 */}
|
{/* Profile summary */}
|
||||||
<div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r-lg)', padding: 20 }}>
|
<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 SlotPicker, { type PickedSlot } from '../components/appointments/SlotPicker';
|
||||||
import { tehranWallClockToUnix, rialToToman, tomanToRial } from '../lib/utils';
|
import { tehranWallClockToUnix, rialToToman, tomanToRial } from '../lib/utils';
|
||||||
import BackButton from '../components/ui/BackButton';
|
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 { digitsOnly, todayIso, formatNumber } from '../lib/utils';
|
||||||
import Switch from '../components/ui/Switch';
|
import Switch from '../components/ui/Switch';
|
||||||
|
|
||||||
@@ -266,6 +268,8 @@ export default function AppointmentCreatePage() {
|
|||||||
onError: (e: any) => toast.error(e.message || 'خطا در ثبت نوبت'),
|
onError: (e: any) => toast.error(e.message || 'خطا در ثبت نوبت'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useTour('appointment-create', { ready: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '20px 24px', maxWidth: 1080, margin: '0 auto' }}>
|
<div style={{ padding: '20px 24px', maxWidth: 1080, margin: '0 auto' }}>
|
||||||
{/* بردکرامب */}
|
{/* بردکرامب */}
|
||||||
@@ -273,7 +277,8 @@ export default function AppointmentCreatePage() {
|
|||||||
<BackButton fallback="/admin/appointments" />
|
<BackButton fallback="/admin/appointments" />
|
||||||
<span style={{ fontSize: 13, color: 'var(--text-3)' }}>نوبت ها</span>
|
<span style={{ fontSize: 13, color: 'var(--text-3)' }}>نوبت ها</span>
|
||||||
<span style={{ 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>
|
</div>
|
||||||
|
|
||||||
{/* فرم باید صریح بگوید این نوبت برای کدام دوره است؛ بیمارِ چنددورهای بدون این،
|
{/* فرم باید صریح بگوید این نوبت برای کدام دوره است؛ بیمارِ چنددورهای بدون این،
|
||||||
@@ -313,7 +318,7 @@ export default function AppointmentCreatePage() {
|
|||||||
{!isDoctor && (
|
{!isDoctor && (
|
||||||
<>
|
<>
|
||||||
<div style={{ ...sectionTitle, marginTop: 0 }}>پزشک:</div>
|
<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 }}>
|
<div style={{ margin: '6px 0 4px', maxWidth: 400 }}>
|
||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
options={doctorOptions}
|
options={doctorOptions}
|
||||||
@@ -386,7 +391,7 @@ export default function AppointmentCreatePage() {
|
|||||||
{/* حالت جستجوی مراجعهکنندهٔ موجود */}
|
{/* حالت جستجوی مراجعهکنندهٔ موجود */}
|
||||||
{!newPatient && (
|
{!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 style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 12, margin: '6px 0 8px' }}>
|
||||||
<div className="field" style={{ width: 400, maxWidth: '100%' }}>
|
<div className="field" style={{ width: 400, maxWidth: '100%' }}>
|
||||||
<MagnifyingGlassIcon style={{ width: 18, height: 18, color: 'var(--text-3)', flexShrink: 0 }} />
|
<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 style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 16, marginBottom: 10 }}>
|
||||||
<div>
|
<div>
|
||||||
<label style={label}>بخش</label>
|
<label style={label} data-tour="create-service">بخش</label>
|
||||||
<div style={{ marginTop: 6 }}>
|
<div style={{ marginTop: 6 }}>
|
||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
inputId="appt-section-select"
|
inputId="appt-section-select"
|
||||||
@@ -702,7 +707,7 @@ export default function AppointmentCreatePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-end', paddingTop: 4 }}>
|
<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 ? '...' : 'ثبت اطلاعات'}
|
{create.isPending ? '...' : 'ثبت اطلاعات'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ export default function AppointmentDetailPage() {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
backTo="/admin/appointments"
|
backTo="/admin/appointments"
|
||||||
title="جزئیات نوبت"
|
title="جزئیات نوبت"
|
||||||
|
tourId="appointment-detail"
|
||||||
|
tourReady={!isLoading}
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
{ label: 'داشبورد', to: '/admin/dashboard' },
|
{ label: 'داشبورد', to: '/admin/dashboard' },
|
||||||
{ label: 'نوبتها', to: backTo },
|
{ label: 'نوبتها', to: backTo },
|
||||||
@@ -122,7 +124,7 @@ export default function AppointmentDetailPage() {
|
|||||||
) : appt ? (
|
) : appt ? (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<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">
|
<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={appt.patient_name} />
|
||||||
<InfoRow label="موبایل" value={<span dir="ltr">{appt.patient_mobile}</span>} />
|
<InfoRow label="موبایل" value={<span dir="ltr">{appt.patient_mobile}</span>} />
|
||||||
<InfoRow label="پزشک" value={appt.doctor?.name ?? null} />
|
<InfoRow label="پزشک" value={appt.doctor?.name ?? null} />
|
||||||
@@ -139,7 +141,7 @@ export default function AppointmentDetailPage() {
|
|||||||
<AppointmentSegmentsCard appointmentUuid={appt.uuid} />
|
<AppointmentSegmentsCard appointmentUuid={appt.uuid} />
|
||||||
|
|
||||||
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-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-status">وضعیت و اقدامات</h3>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<p className="text-sm text-[var(--text-2)] mb-2">وضعیت فعلی:</p>
|
<p className="text-sm text-[var(--text-2)] mb-2">وضعیت فعلی:</p>
|
||||||
<StatusBadge type="appointment" value={appt.status} />
|
<StatusBadge type="appointment" value={appt.status} />
|
||||||
@@ -198,7 +200,7 @@ export default function AppointmentDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6 lg:col-span-2">
|
<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 ? (
|
{eventsQuery.isLoading ? (
|
||||||
<div className="h-6 w-40 rounded skeleton" />
|
<div className="h-6 w-40 rounded skeleton" />
|
||||||
) : events.length === 0 ? (
|
) : events.length === 0 ? (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import SearchableSelect from '../components/ui/SearchableSelect';
|
|||||||
import { formatRial, rialToToman, tomanToRial } from '../lib/utils';
|
import { formatRial, rialToToman, tomanToRial } from '../lib/utils';
|
||||||
import { DEFAULT_SERVICE_CATEGORY } from '../lib/insuranceShares';
|
import { DEFAULT_SERVICE_CATEGORY } from '../lib/insuranceShares';
|
||||||
import BackButton from '../components/ui/BackButton';
|
import BackButton from '../components/ui/BackButton';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
import { useAppointmentInsurance } from '../hooks/useAppointmentInsurance';
|
import { useAppointmentInsurance } from '../hooks/useAppointmentInsurance';
|
||||||
import { useDoctorBookingServices } from '../hooks/useDoctorBookingServices';
|
import { useDoctorBookingServices } from '../hooks/useDoctorBookingServices';
|
||||||
import ServiceSlotPicker from '../components/appointments/ServiceSlotPicker';
|
import ServiceSlotPicker from '../components/appointments/ServiceSlotPicker';
|
||||||
@@ -194,12 +195,13 @@ export default function AppointmentEditPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fade-in" style={{ maxWidth: 860, margin: '0 auto' }}>
|
<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}`} />
|
<BackButton fallback={`/admin/appointments?date=${date}`} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r-lg)', padding: 22 }}>
|
<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 style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 12, marginBottom: 18 }}>
|
||||||
<div>
|
<div>
|
||||||
<label style={label}>بخش</label>
|
<label style={label}>بخش</label>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { ScheduleSection } from '../components/schedule/ScheduleSection';
|
|||||||
import type { AddressData } from '../components/schedule/ScheduleSection';
|
import type { AddressData } from '../components/schedule/ScheduleSection';
|
||||||
import SearchableSelect from '../components/ui/SearchableSelect';
|
import SearchableSelect from '../components/ui/SearchableSelect';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
const PERSONAL = 'personal';
|
const PERSONAL = 'personal';
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ export default function AppointmentSettingsPage() {
|
|||||||
style={{ background: 'var(--surface)', minWidth: 0 }}
|
style={{ background: 'var(--surface)', minWidth: 0 }}
|
||||||
className="px-4 py-4 md:px-6 md:py-6 rounded-[var(--r-lg)]"
|
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} />
|
<FreeVisitPrice readOnly={apptReadOnly} />
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export default function CatalogCategoriesPage() {
|
|||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="دستهبندیها"
|
title="دستهبندیها"
|
||||||
|
tourId="service-categories"
|
||||||
description="دستهبندی سراسری کلینیک؛ یک بار تعریف میشود و سرویسها و منابع از همینها انتخاب میکنند."
|
description="دستهبندی سراسری کلینیک؛ یک بار تعریف میشود و سرویسها و منابع از همینها انتخاب میکنند."
|
||||||
backTo="/admin/settings-menu"
|
backTo="/admin/settings-menu"
|
||||||
action={
|
action={
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ export default function ClaimPatientDetailPage() {
|
|||||||
<FeatureGate feature="insurance">
|
<FeatureGate feature="insurance">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
backTo="/admin/claims"
|
backTo="/admin/claims"
|
||||||
|
tourId="claim-detail"
|
||||||
title={patient?.full_name ?? 'پرونده بیمه بیمار'}
|
title={patient?.full_name ?? 'پرونده بیمه بیمار'}
|
||||||
description={[patient?.mobile, patient?.national_code].filter(Boolean).join(' · ') || undefined}
|
description={[patient?.mobile, patient?.national_code].filter(Boolean).join(' · ') || undefined}
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
@@ -202,7 +203,8 @@ export default function ClaimPatientDetailPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal open={!!selected} title="جزئیات مطالبه" size="lg" onClose={() => setSelected(null)}>
|
<Modal open={!!selected} title="جزئیات مطالبه"
|
||||||
|
size="lg" onClose={() => setSelected(null)}>
|
||||||
{selected && (
|
{selected && (
|
||||||
<div dir="rtl" style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
|
<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 }}>
|
<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">
|
<FeatureGate feature="insurance">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="پروندههای بیمه"
|
title="پروندههای بیمه"
|
||||||
|
tourId="claims"
|
||||||
description="پیگیری مطالبات بیمه به تفکیک بیمار"
|
description="پیگیری مطالبات بیمه به تفکیک بیمار"
|
||||||
breadcrumbs={[{ label: 'داشبورد', to: '/admin' }, { label: 'پروندههای بیمه' }]}
|
breadcrumbs={[{ label: 'داشبورد', to: '/admin' }, { label: 'پروندههای بیمه' }]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ function ClinicAppointmentSettingsContent() {
|
|||||||
در دید، بدون اسکرول. پایینتر از هدر، کاربر باید دنبالش میگشت. */}
|
در دید، بدون اسکرول. پایینتر از هدر، کاربر باید دنبالش میگشت. */}
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="مدیریت نوبتدهی"
|
title="مدیریت نوبتدهی"
|
||||||
|
tourId="clinic-appointment-settings"
|
||||||
description={description}
|
description={description}
|
||||||
backTo="/admin/settings-menu"
|
backTo="/admin/settings-menu"
|
||||||
action={(
|
action={(
|
||||||
|
|||||||
@@ -587,6 +587,7 @@ export default function ClinicDetailPage() {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
backTo="/admin/clinics"
|
backTo="/admin/clinics"
|
||||||
title={clinicName}
|
title={clinicName}
|
||||||
|
tourId="clinic-detail"
|
||||||
breadcrumbs={[{ label: 'کلینیکها', to: '/admin/clinics' }, { label: clinicName }]}
|
breadcrumbs={[{ label: 'کلینیکها', to: '/admin/clinics' }, { label: clinicName }]}
|
||||||
action={
|
action={
|
||||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
|
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ function ClinicDoctorsContent() {
|
|||||||
{/* عنوان و بازگشت و اکشن هر سه در PageHeader — همان قاعدهٔ بقیهٔ صفحات پنل. */}
|
{/* عنوان و بازگشت و اکشن هر سه در PageHeader — همان قاعدهٔ بقیهٔ صفحات پنل. */}
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="پزشکان کلینیک"
|
title="پزشکان کلینیک"
|
||||||
|
tourId="clinic-doctors"
|
||||||
description="مدیریت پزشکان و دعوتنامههای کلینیک"
|
description="مدیریت پزشکان و دعوتنامههای کلینیک"
|
||||||
backTo="/admin/settings-menu"
|
backTo="/admin/settings-menu"
|
||||||
action={
|
action={
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ function ClinicServicesPageInner() {
|
|||||||
که نه دکمهٔ بازگشت دارد و نه میگوید چیست. */}
|
که نه دکمهٔ بازگشت دارد و نه میگوید چیست. */}
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="سرویسها"
|
title="سرویسها"
|
||||||
|
tourId="clinic-services"
|
||||||
description="بخشها و سرویسهای قابل رزرو. مدت و قیمت هر سرویس از همینجا میآید."
|
description="بخشها و سرویسهای قابل رزرو. مدت و قیمت هر سرویس از همینجا میآید."
|
||||||
backTo="/admin/settings-menu"
|
backTo="/admin/settings-menu"
|
||||||
action={
|
action={
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import Pagination from '../components/ui/Pagination';
|
|||||||
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||||
import ChangeLoginMobileModal from '../components/ChangeLoginMobileModal';
|
import ChangeLoginMobileModal from '../components/ChangeLoginMobileModal';
|
||||||
import { latinDigitsField } from '../lib/forms';
|
import { latinDigitsField } from '../lib/forms';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
const HUES_LIST = [256, 205, 162, 295, 272];
|
const HUES_LIST = [256, 205, 162, 295, 272];
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ export default function ClinicsPage() {
|
|||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
||||||
<div>
|
<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 className="muted">{formatNumber(total)} کلینیک ثبتشده</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn primary sm" onClick={() => setAddOpen(true)}>
|
<button className="btn primary sm" onClick={() => setAddOpen(true)}>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import DoctorAppointmentsPanel from '../components/dashboard/DoctorAppointmentsP
|
|||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
import { STATUS_META } from '../components/ui/AppointmentStatusDropdown';
|
import { STATUS_META } from '../components/ui/AppointmentStatusDropdown';
|
||||||
import StatusBadge from '../components/ui/StatusBadge';
|
import StatusBadge from '../components/ui/StatusBadge';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
// ── Chart period (Jalali) ─────────────────────────────────────────────────
|
// ── Chart period (Jalali) ─────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -437,7 +438,7 @@ function AdminDashboard() {
|
|||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
||||||
<div>
|
<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 className="muted" style={{ fontSize: 13, marginTop: 2 }}>{today} · نمای کلی عملکرد مجموعه</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
import SettingsLayout from '../components/layout/SettingsLayout';
|
||||||
import DiscountTab from '../components/DiscountTab';
|
import DiscountTab from '../components/DiscountTab';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
/** مدیریت تخفیفها — بخش تنظیمات برای تعریف قوانین تخفیف عمومی (owner-scoped). */
|
/** مدیریت تخفیفها — بخش تنظیمات برای تعریف قوانین تخفیف عمومی (owner-scoped). */
|
||||||
export default function DiscountsPage() {
|
export default function DiscountsPage() {
|
||||||
@@ -9,8 +10,11 @@ export default function DiscountsPage() {
|
|||||||
style={{ background: 'var(--surface)', minWidth: 0 }}
|
style={{ background: 'var(--surface)', minWidth: 0 }}
|
||||||
className="px-4 py-4 md:px-6 md:py-6 rounded-[var(--r-lg)]"
|
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, marginBottom: 16 }} data-tour="page-title">
|
||||||
<DiscountTab />
|
<h1 className="section-title">مدیریت تخفیفها</h1>
|
||||||
|
<TourButton tourId="discounts" ready />
|
||||||
|
</div>
|
||||||
|
<div data-tour="discounts-rules"><DiscountTab /></div>
|
||||||
</div>
|
</div>
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { latinDigitsField } from '../lib/forms';
|
|||||||
import BackButton from '../components/ui/BackButton';
|
import BackButton from '../components/ui/BackButton';
|
||||||
import { toggleSpecialtyChild, toggleSpecialtyRoot, removeSpecialtyEntry } from '../lib/specialtySelection';
|
import { toggleSpecialtyChild, toggleSpecialtyRoot, removeSpecialtyEntry } from '../lib/specialtySelection';
|
||||||
import Switch from '../components/ui/Switch';
|
import Switch from '../components/ui/Switch';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
// Fix leaflet default marker icons
|
// Fix leaflet default marker icons
|
||||||
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
||||||
@@ -1432,7 +1433,10 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0 sm:mb-1">
|
<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">
|
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
|
||||||
{/* Active status badge */}
|
{/* Active status badge */}
|
||||||
{doctor.active
|
{doctor.active
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import PersianDatePicker from '../components/ui/PersianDatePicker';
|
|||||||
import { iranMobileSchema } from '../lib/utils';
|
import { iranMobileSchema } from '../lib/utils';
|
||||||
import BackButton from '../components/ui/BackButton';
|
import BackButton from '../components/ui/BackButton';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
// ── Types ────────────────────────────────────────────────────────────────────
|
// ── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -319,10 +320,11 @@ export default function DoctorFormPage() {
|
|||||||
پزشکان
|
پزشکان
|
||||||
</button>
|
</button>
|
||||||
<span>/</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)} data-tour="doctor-form">
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
|
|
||||||
{/* ── Section: اطلاعات حساب ──────────────────────────────── */}
|
{/* ── Section: اطلاعات حساب ──────────────────────────────── */}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import ChangeLoginMobileModal from '../components/ChangeLoginMobileModal';
|
|||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import SearchableSelect from '../components/ui/SearchableSelect';
|
import SearchableSelect from '../components/ui/SearchableSelect';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
// ── Types ─────────────────────────────────────────────────────────────────
|
// ── Types ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -239,7 +240,7 @@ export default function DoctorsPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
||||||
<div>
|
<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 className="muted" style={{ fontSize: 13, marginTop: 3 }}>مدیریت پزشکان، تخصصها و وضعیت همکاری</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 10 }}>
|
<div style={{ display: 'flex', gap: 10 }}>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import CreateStep from '../components/session/CreateStep';
|
|||||||
import PaymentStep from '../components/session/PaymentStep';
|
import PaymentStep from '../components/session/PaymentStep';
|
||||||
import { CloseModalD } from '../components/icons/FilesServiceIcons';
|
import { CloseModalD } from '../components/icons/FilesServiceIcons';
|
||||||
import { Breadcrumb } from '../components/PatientCaseBanner';
|
import { Breadcrumb } from '../components/PatientCaseBanner';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
/** ویرایش مراجعهی ثبتشده — دو تب: ویرایش سرویسها + مدیریت پرداختها. */
|
/** ویرایش مراجعهی ثبتشده — دو تب: ویرایش سرویسها + مدیریت پرداختها. */
|
||||||
export default function EditSessionPage() {
|
export default function EditSessionPage() {
|
||||||
@@ -47,7 +48,10 @@ export default function EditSessionPage() {
|
|||||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
|
<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)]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
<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)}
|
<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' }}>
|
style={{ minWidth: 40, height: 40, borderRadius: '50%', border: 'none', background: 'transparent', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<CloseModalD />
|
<CloseModalD />
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export default function HolidaysSettingsPage() {
|
|||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="تعطیلات رسمی"
|
title="تعطیلات رسمی"
|
||||||
|
tourId="holidays"
|
||||||
description="تعطیلات کشوری برای همهٔ محیطها اعمال میشود. اگر این محیط روزی را باز است، همینجا استثنا بزنید."
|
description="تعطیلات کشوری برای همهٔ محیطها اعمال میشود. اگر این محیط روزی را باز است، همینجا استثنا بزنید."
|
||||||
backTo="/admin/settings-menu"
|
backTo="/admin/settings-menu"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default function InsurancePricingPage() {
|
|||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="مدیریت بیمه"
|
title="مدیریت بیمه"
|
||||||
|
tourId="insurance-pricing"
|
||||||
description="قراردادهای بیمه پایه و تکمیلی"
|
description="قراردادهای بیمه پایه و تکمیلی"
|
||||||
/>
|
/>
|
||||||
<InsuranceServiceCategoriesCard />
|
<InsuranceServiceCategoriesCard />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import SearchableSelect from '../components/ui/SearchableSelect';
|
|||||||
import { useInventory } from '../hooks/useInventory';
|
import { useInventory } from '../hooks/useInventory';
|
||||||
import type { InventoryItem, InventoryPackage, ItemPayload, PackagePayload } from '../hooks/useInventory';
|
import type { InventoryItem, InventoryPackage, ItemPayload, PackagePayload } from '../hooks/useInventory';
|
||||||
import InventoryStatCards from '../components/inventory/InventoryStatCards';
|
import InventoryStatCards from '../components/inventory/InventoryStatCards';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
import InventoryItemsTable from '../components/inventory/InventoryItemsTable';
|
import InventoryItemsTable from '../components/inventory/InventoryItemsTable';
|
||||||
import PackagesView from '../components/inventory/PackagesView';
|
import PackagesView from '../components/inventory/PackagesView';
|
||||||
import AddItemModal from '../components/inventory/AddItemModal';
|
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 }}>
|
<div className="fade-in" style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div>
|
||||||
<h1 style={{ fontSize: 18, fontWeight: 800, color: 'var(--text)', marginBottom: 12 }}>انبارداری</h1>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 12 }} data-tour="page-title">
|
||||||
<div className="inv-tabs">
|
<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 === 'stock' ? ' active' : ''}`} onClick={() => setTab('stock')}>کالاهای مصرفی</button>
|
||||||
<button className={`inv-tab${tab === 'packages' ? ' active' : ''}`} onClick={() => setTab('packages')}>پکیج</button>
|
<button className={`inv-tab${tab === 'packages' ? ' active' : ''}`} onClick={() => setTab('packages')}>پکیج</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,7 +109,7 @@ export default function InventoryPage() {
|
|||||||
{/* Body */}
|
{/* Body */}
|
||||||
{tab === 'stock' ? (
|
{tab === 'stock' ? (
|
||||||
<>
|
<>
|
||||||
<InventoryStatCards stats={stats} />
|
<div data-tour="inventory-stats"><InventoryStatCards stats={stats} /></div>
|
||||||
{itemsLoading ? (
|
{itemsLoading ? (
|
||||||
<div style={{ padding: 16, color: 'var(--text-3)', fontSize: 13 }}>در حال بارگذاری...</div>
|
<div style={{ padding: 16, color: 'var(--text-3)', fontSize: 13 }}>در حال بارگذاری...</div>
|
||||||
) : filteredItems.length === 0 ? (
|
) : filteredItems.length === 0 ? (
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ function MyFinancialPageContent() {
|
|||||||
<div className="page">
|
<div className="page">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="مدیریت پرداخت"
|
title="مدیریت پرداخت"
|
||||||
|
tourId="my-financial"
|
||||||
description={`روشهای پرداخت «${environmentName}» (حساب بانکی و کارتخوان)`}
|
description={`روشهای پرداخت «${environmentName}» (حساب بانکی و کارتخوان)`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -606,6 +606,8 @@ function MyPatientsPageInner() {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="پرونده بیماران"
|
title="پرونده بیماران"
|
||||||
description="مراجعهکنندگان ثبتشده شما"
|
description="مراجعهکنندگان ثبتشده شما"
|
||||||
|
tourId="my-patients"
|
||||||
|
tourReady={!isLoading}
|
||||||
action={
|
action={
|
||||||
canCreate ? (
|
canCreate ? (
|
||||||
<button
|
<button
|
||||||
@@ -621,6 +623,7 @@ function MyPatientsPageInner() {
|
|||||||
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<div
|
<div
|
||||||
|
data-tour="patients-search"
|
||||||
style={{
|
style={{
|
||||||
background: "var(--surface)",
|
background: "var(--surface)",
|
||||||
border: "1px solid var(--border)",
|
border: "1px solid var(--border)",
|
||||||
@@ -709,6 +712,7 @@ function MyPatientsPageInner() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
data-tour="patients-records"
|
||||||
style={
|
style={
|
||||||
viewMode === "grid"
|
viewMode === "grid"
|
||||||
? { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: 14 }
|
? { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: 14 }
|
||||||
@@ -1025,6 +1029,7 @@ function MyPatientsPageInner() {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title={selectedPatientName}
|
title={selectedPatientName}
|
||||||
description={`تلفن: ${selectedPatientPhone} — پرونده از ${formatDate(selectedRecord.created_at)}`}
|
description={`تلفن: ${selectedPatientPhone} — پرونده از ${formatDate(selectedRecord.created_at)}`}
|
||||||
|
tourId="my-patient-record"
|
||||||
action={
|
action={
|
||||||
<div style={{ display: "flex", gap: 8 }}>
|
<div style={{ display: "flex", gap: 8 }}>
|
||||||
<button
|
<button
|
||||||
@@ -1047,6 +1052,7 @@ function MyPatientsPageInner() {
|
|||||||
|
|
||||||
{/* بنر اطلاعات بیمار — مطابق فیگما */}
|
{/* بنر اطلاعات بیمار — مطابق فیگما */}
|
||||||
<div
|
<div
|
||||||
|
data-tour="record-banner"
|
||||||
style={{
|
style={{
|
||||||
background: "var(--surface)",
|
background: "var(--surface)",
|
||||||
border: "1px solid var(--border)",
|
border: "1px solid var(--border)",
|
||||||
@@ -1130,6 +1136,7 @@ function MyPatientsPageInner() {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className="cp-tabs"
|
className="cp-tabs"
|
||||||
|
data-tour="record-tabs"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: 8,
|
gap: 8,
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ describe('MyPaymentDetailPage (پرداختهای ثبتشده)', () => {
|
|||||||
it('fetches invoices for the patient uuid from the route', async () => {
|
it('fetches invoices for the patient uuid from the route', async () => {
|
||||||
renderPage();
|
renderPage();
|
||||||
await screen.findAllByText('دنیا خلیلی');
|
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 () => {
|
it('expands a row to show its line items and payment methods on بیشتر', async () => {
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export default function MyPaymentDetailPage() {
|
|||||||
<>
|
<>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
backTo="/admin/my-payments"
|
backTo="/admin/my-payments"
|
||||||
|
tourId="my-payment-detail"
|
||||||
title="پرداختهای ثبتشده"
|
title="پرداختهای ثبتشده"
|
||||||
description="صورتحسابهای ثبتشدهی این بیمار"
|
description="صورتحسابهای ثبتشدهی این بیمار"
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ export default function MyPaymentsPage() {
|
|||||||
<>
|
<>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="لیست پرداختها"
|
title="لیست پرداختها"
|
||||||
|
tourId="my-payments"
|
||||||
description="پرداختهای ثبتشدهی بیماران شما"
|
description="پرداختهای ثبتشدهی بیماران شما"
|
||||||
breadcrumbs={[{ label: 'داشبورد', to: '/admin' }, { label: 'لیست پرداختها' }]}
|
breadcrumbs={[{ label: 'داشبورد', to: '/admin' }, { label: 'لیست پرداختها' }]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useAuthStore } from "../stores/authStore";
|
|||||||
import type { Secretary, SecretaryPermissions } from "../types";
|
import type { Secretary, SecretaryPermissions } from "../types";
|
||||||
import SecretaryPermissionsModal from "../components/SecretaryPermissionsModal";
|
import SecretaryPermissionsModal from "../components/SecretaryPermissionsModal";
|
||||||
import Switch from '../components/ui/Switch';
|
import Switch from '../components/ui/Switch';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
// ── SVG icons (copied verbatim from clinic-pro-tauri) ───────────────────────
|
// ── SVG icons (copied verbatim from clinic-pro-tauri) ───────────────────────
|
||||||
|
|
||||||
@@ -815,12 +816,15 @@ function MySecretariesPageContent() {
|
|||||||
return (
|
return (
|
||||||
<div dir="rtl">
|
<div dir="rtl">
|
||||||
<div className="flex items-center justify-between">
|
<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>
|
||||||
|
|
||||||
{/* تبها + دکمه افزودن */}
|
{/* تبها + دکمه افزودن */}
|
||||||
<div className="w-full flex items-end justify-between mt-[20px]">
|
<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) => (
|
{["منشی های فعلی", "منشی های قبلی"].map((label, idx) => (
|
||||||
<button
|
<button
|
||||||
key={idx}
|
key={idx}
|
||||||
@@ -837,6 +841,7 @@ function MySecretariesPageContent() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
data-tour="secretaries-add"
|
||||||
onClick={handleAddClick}
|
onClick={handleAddClick}
|
||||||
className="shadow-none gap-[8px] bg-[var(--primary)] text-[var(--on-primary)] text-[14px] md:text-[15px] lg:text-[16px]
|
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
|
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 DetailsStep from '../components/session/DetailsStep';
|
||||||
import { CloseModalD } from '../components/icons/FilesServiceIcons';
|
import { CloseModalD } from '../components/icons/FilesServiceIcons';
|
||||||
import { Breadcrumb } from '../components/PatientCaseBanner';
|
import { Breadcrumb } from '../components/PatientCaseBanner';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ثبت مراجعه جدید — پورت کامل tauri /files/create-service (حالت ایجاد):
|
* ثبت مراجعه جدید — پورت کامل tauri /files/create-service (حالت ایجاد):
|
||||||
@@ -53,11 +54,14 @@ export default function NewSessionPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fade-in" style={{ width: '100%' }}>
|
<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 */}
|
{/* card — tauri width 748 centered */}
|
||||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
|
<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 }}>
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import Modal from '../components/ui/Modal';
|
|||||||
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||||
import PersianDateInput from '../components/ui/PersianDateInput';
|
import PersianDateInput from '../components/ui/PersianDateInput';
|
||||||
import PatientCaseBanner, { Breadcrumb } from '../components/PatientCaseBanner';
|
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 SessionServiceCard, { type SessionCardData } from '../components/SessionServiceCard';
|
||||||
import { useIssueInvoice } from '../hooks/useIssueInvoice';
|
import { useIssueInvoice } from '../hooks/useIssueInvoice';
|
||||||
import AppointmentTurnCard, { type AppointmentCardData } from '../components/AppointmentTurnCard';
|
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'))
|
.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;
|
.sort((a: any, b: any) => a.starts_at - b.starts_at)[0]?.starts_at ?? null;
|
||||||
|
|
||||||
|
useTour('patient-detail', { ready: !isLoading });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
{/* breadcrumb + patient banner (tauri BreadcrumbHeader + FileServicesHeader) */}
|
{/* 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
|
<PatientCaseBanner
|
||||||
name={record?.user_name || 'پرونده'}
|
name={record?.user_name || 'پرونده'}
|
||||||
recordNumber={record?.record_number}
|
recordNumber={record?.record_number}
|
||||||
@@ -215,9 +223,10 @@ export default function PatientDetailPage() {
|
|||||||
hasDebt={hasDebt}
|
hasDebt={hasDebt}
|
||||||
onAddNote={() => setTab('notes')}
|
onAddNote={() => setTab('notes')}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* tab bar */}
|
{/* 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) => {
|
{TABS.map((t) => {
|
||||||
const on = t.key === tab;
|
const on = t.key === tab;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { numericField } from '../lib/forms';
|
|||||||
import BackButton from '../components/ui/BackButton';
|
import BackButton from '../components/ui/BackButton';
|
||||||
import { iranNationalCodeSchema, iranMobileSchema, unixToIso } from '../lib/utils';
|
import { iranNationalCodeSchema, iranMobileSchema, unixToIso } from '../lib/utils';
|
||||||
import { useRecordNumberSettings } from '../hooks/useRecordNumberSettings';
|
import { useRecordNumberSettings } from '../hooks/useRecordNumberSettings';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
const REFERRAL_OPTIONS = ['اینستاگرام', 'معرفی دوستان و آشنایان', 'جستجوی اینترنتی', 'تابلو مطب', 'سایر'];
|
const REFERRAL_OPTIONS = ['اینستاگرام', 'معرفی دوستان و آشنایان', 'جستجوی اینترنتی', 'تابلو مطب', 'سایر'];
|
||||||
|
|
||||||
@@ -131,10 +132,13 @@ export default function PatientRecordFormPage() {
|
|||||||
<div className="fade-in" style={{ maxWidth: 1000, margin: '0 auto' }}>
|
<div className="fade-in" style={{ maxWidth: 1000, margin: '0 auto' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 18 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 18 }}>
|
||||||
<BackButton fallback="/admin/patients" />
|
<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>
|
</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 }}>
|
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 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 18 }}>
|
||||||
<Field label="نام و نام خانوادگی مراجعه کننده" required error={form.formState.errors.name?.message}>
|
<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 PatientTagsCell from '../components/PatientTagsCell';
|
||||||
import PatientsFilterModal, { type PatientFilters } from '../components/PatientsFilterModal';
|
import PatientsFilterModal, { type PatientFilters } from '../components/PatientsFilterModal';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
|
import { useTour } from '../hooks/useTour';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
import {
|
import {
|
||||||
SearchHeaderP, TurnsFilter, PatientsGridView, PatientsCategoryView, AddTurn,
|
SearchHeaderP, TurnsFilter, PatientsGridView, PatientsCategoryView, AddTurn,
|
||||||
} from '../components/icons/FilesToolbarIcons';
|
} from '../components/icons/FilesToolbarIcons';
|
||||||
@@ -161,6 +163,8 @@ export default function PatientsListPage() {
|
|||||||
queryFn: () => api.get(`/api/v1/patients?${qs.toString()}`),
|
queryFn: () => api.get(`/api/v1/patients?${qs.toString()}`),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useTour('patients', { ready: !isLoading });
|
||||||
|
|
||||||
const activeFilters = countFilters(filters);
|
const activeFilters = countFilters(filters);
|
||||||
const records = data?.data ?? EMPTY;
|
const records = data?.data ?? EMPTY;
|
||||||
const total = data?.meta?.totalRecords ?? 0;
|
const total = data?.meta?.totalRecords ?? 0;
|
||||||
@@ -183,14 +187,17 @@ export default function PatientsListPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
{/* Head — تیتر تنها (tauri files/head) */}
|
{/* 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) */}
|
{/* Inputs — ردیف کنترلها (tauri files/inputs) */}
|
||||||
<div className="my-[16px] md:my-[20px] lg:my-[24px]">
|
<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 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="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 }}>
|
<div className="flex items-center rounded-[6px] border border-[var(--border)] bg-[var(--bg)]" style={{ height: 48, paddingInline: 4 }}>
|
||||||
<input
|
<input
|
||||||
dir="rtl"
|
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 }}>
|
<div className="flex items-center w-full lg:w-auto justify-between lg:justify-end" style={{ gap: 12 }}>
|
||||||
<button
|
<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"
|
className="flex items-center justify-center rounded-[4px] cursor-pointer"
|
||||||
style={{ width: 62, height: 48, border: '1px solid var(--primary)', background: 'transparent', position: 'relative' }}
|
style={{ width: 62, height: 48, border: '1px solid var(--primary)', background: 'transparent', position: 'relative' }}
|
||||||
>
|
>
|
||||||
@@ -222,7 +229,7 @@ export default function PatientsListPage() {
|
|||||||
</button>
|
</button>
|
||||||
{canCreate && (
|
{canCreate && (
|
||||||
<button
|
<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"
|
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' }}
|
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 { useAuthStore } from '../stores/authStore';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
import type { PracticeDomain } from '../types';
|
import type { PracticeDomain } from '../types';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* حوزهٔ فعالیت کلینیک.
|
* حوزهٔ فعالیت کلینیک.
|
||||||
@@ -62,7 +63,7 @@ export default function PracticeDomainSettingsPage() {
|
|||||||
// منوی تنظیمات را نشان نمیداد و کاربر برای رفتن به بخش بعدی باید «بازگشت» میزد.
|
// منوی تنظیمات را نشان نمیداد و کاربر برای رفتن به بخش بعدی باید «بازگشت» میزد.
|
||||||
<SettingsLayout active="practice-domain">
|
<SettingsLayout active="practice-domain">
|
||||||
<div className="card card-pad" style={{ display: 'grid', gap: 14, maxWidth: 560 }}>
|
<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 }}>
|
<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 { useRecordNumberSettings } from '../hooks/useRecordNumberSettings';
|
||||||
import type { RecordNumberResetPolicy } from '../hooks/useRecordNumberSettings';
|
import type { RecordNumberResetPolicy } from '../hooks/useRecordNumberSettings';
|
||||||
import Switch from '../components/ui/Switch';
|
import Switch from '../components/ui/Switch';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
const RESET_OPTIONS: { value: RecordNumberResetPolicy; label: string }[] = [
|
const RESET_OPTIONS: { value: RecordNumberResetPolicy; label: string }[] = [
|
||||||
{ value: 'none', label: 'هرگز — شمارنده پیوسته جلو میرود' },
|
{ value: 'none', label: 'هرگز — شمارنده پیوسته جلو میرود' },
|
||||||
@@ -49,7 +50,7 @@ export default function RecordNumberSettingsPage() {
|
|||||||
style={{ background: 'var(--surface)', minWidth: 0 }}
|
style={{ background: 'var(--surface)', minWidth: 0 }}
|
||||||
className="px-4 py-4 md:px-6 md:py-6 rounded-[var(--r-lg)]"
|
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 }}>
|
<p style={{ fontSize: 12.5, color: 'var(--text-3)', marginBottom: 18, lineHeight: 2 }}>
|
||||||
با روشنکردن این گزینه، شمارهٔ هر پروندهٔ تازه — چه از فرم تشکیل پرونده و چه از
|
با روشنکردن این گزینه، شمارهٔ هر پروندهٔ تازه — چه از فرم تشکیل پرونده و چه از
|
||||||
نوبتی که قطعی میشود — بر اساس همین الگو ساخته میشود.
|
نوبتی که قطعی میشود — بر اساس همین الگو ساخته میشود.
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export default function RepresentationBlogFormPage() {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
backTo="/admin/representation-blogs"
|
backTo="/admin/representation-blogs"
|
||||||
title={isEdit ? 'ویرایش مقاله' : 'نوشتن مقالهٔ جدید'}
|
title={isEdit ? 'ویرایش مقاله' : 'نوشتن مقالهٔ جدید'}
|
||||||
|
tourId="representation-blog-form"
|
||||||
breadcrumbs={[{ label: 'وبلاگ من', to: '/admin/representation-blogs' }, { label: isEdit ? 'ویرایش' : 'جدید' }]}
|
breadcrumbs={[{ label: 'وبلاگ من', to: '/admin/representation-blogs' }, { label: isEdit ? 'ویرایش' : 'جدید' }]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export default function RepresentationBlogsPage() {
|
|||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="وبلاگ من"
|
title="وبلاگ من"
|
||||||
|
tourId="representation-blogs"
|
||||||
description="مقالات مخصوص دامنه و برند شما."
|
description="مقالات مخصوص دامنه و برند شما."
|
||||||
action={<button className="cp-btn-primary" onClick={() => navigate('/admin/representation-blogs/new')}>مقالهٔ جدید</button>}
|
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 type { ApiResponse, PaginatedResponse } from '../lib/api';
|
||||||
import { formatRial, formatNumber, formatDate } from '../lib/utils';
|
import { formatRial, formatNumber, formatDate } from '../lib/utils';
|
||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
interface FinanceRow {
|
interface FinanceRow {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
@@ -79,7 +80,7 @@ export default function RepresentationFinancePage() {
|
|||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
||||||
<div>
|
<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 className="muted" style={{ fontSize: 13, marginTop: 3 }}>
|
||||||
درآمد ثبتشده از پورسانت نوبتها — تفکیک هر تراکنش
|
درآمد ثبتشده از پورسانت نوبتها — تفکیک هر تراکنش
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { api } from '../lib/api';
|
|||||||
import type { ApiResponse } from '../lib/api';
|
import type { ApiResponse } from '../lib/api';
|
||||||
import PersianDatePicker from '../components/ui/PersianDatePicker';
|
import PersianDatePicker from '../components/ui/PersianDatePicker';
|
||||||
import { toEnglishDigits, digitsOnly } from '../lib/utils';
|
import { toEnglishDigits, digitsOnly } from '../lib/utils';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
// تبدیل تاریخ میلادی ISO (YYYY-MM-DD) به شمسی Y/m/d برای استعلام api.ir
|
// تبدیل تاریخ میلادی ISO (YYYY-MM-DD) به شمسی Y/m/d برای استعلام api.ir
|
||||||
function toJalali(iso: string): string {
|
function toJalali(iso: string): string {
|
||||||
@@ -126,7 +127,7 @@ export default function RepresentationProfilePage() {
|
|||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
||||||
<div>
|
<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 className="muted" style={{ fontSize: 13, marginTop: 2 }}>تأیید هویت و مدیریت شمارههای شبا</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { formatRial, formatDate, tomanToRial } from '../lib/utils';
|
|||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import SearchableSelect from '../components/ui/SearchableSelect';
|
import SearchableSelect from '../components/ui/SearchableSelect';
|
||||||
import { digitsOnly } from '../lib/utils';
|
import { digitsOnly } from '../lib/utils';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
interface WalletBalance { balance_rials: number }
|
interface WalletBalance { balance_rials: number }
|
||||||
interface RepSummary { income: { settlable_rials: number; settled_rials: number; pending_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="fade-in">
|
||||||
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
||||||
<div>
|
<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 className="muted" style={{ fontSize: 13, marginTop: 2 }}>درخواست برداشت از کیفپول نماینده</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import SearchableSelect from '../components/ui/SearchableSelect';
|
|||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import NewAppointmentDrawer from '../components/NewAppointmentDrawer';
|
import NewAppointmentDrawer from '../components/NewAppointmentDrawer';
|
||||||
import BackButton from '../components/ui/BackButton';
|
import BackButton from '../components/ui/BackButton';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
import { useTour } from '../hooks/useTour';
|
||||||
import { AppointmentInfoModal, TransferReserveModal } from '../components/AppointmentActions';
|
import { AppointmentInfoModal, TransferReserveModal } from '../components/AppointmentActions';
|
||||||
|
|
||||||
const LIMIT = 20;
|
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 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' };
|
const td: React.CSSProperties = { padding: '10px 14px', textAlign: 'right', color: 'var(--text)', verticalAlign: 'middle' };
|
||||||
|
|
||||||
|
useTour('appointments-reserve', { ready: !isLoading });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fade-in" style={{ padding: '20px 24px' }}>
|
<div className="fade-in" style={{ padding: '20px 24px' }}>
|
||||||
<div style={{ marginBottom: 14 }}>
|
<div style={{ marginBottom: 14 }}>
|
||||||
<BackButton fallback="/admin/appointments" />
|
<BackButton fallback="/admin/appointments" />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 10, marginBottom: 16 }}>
|
<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 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
{isClinic && (
|
{isClinic && (
|
||||||
<div style={{ minWidth: 180 }}>
|
<div style={{ minWidth: 180 }} data-tour="reserve-doctor">
|
||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
options={clinicDoctors.map(d => ({ value: d.uuid, label: d.name }))}
|
options={clinicDoctors.map(d => ({ value: d.uuid, label: d.name }))}
|
||||||
value={doctorUuid || null}
|
value={doctorUuid || null}
|
||||||
@@ -140,7 +147,7 @@ export default function ReserveAppointmentsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{primaryRole !== 'representation' && (
|
{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 }}>
|
onClick={() => setDrawerOpen(true)} style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
|
||||||
<PlusIcon style={{ width: 15 }} /> نوبت رزرو
|
<PlusIcon style={{ width: 15 }} /> نوبت رزرو
|
||||||
</button>
|
</button>
|
||||||
@@ -148,7 +155,7 @@ export default function ReserveAppointmentsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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 ? (
|
{isLoading ? (
|
||||||
<div style={{ padding: 40, textAlign: 'center', color: 'var(--text-3)' }}>در حال بارگذاری...</div>
|
<div style={{ padding: 40, textAlign: 'center', color: 'var(--text-3)' }}>در حال بارگذاری...</div>
|
||||||
) : items.length === 0 ? (
|
) : items.length === 0 ? (
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export default function ResourceDetailPage() {
|
|||||||
title={resource.name}
|
title={resource.name}
|
||||||
description={`${resource.type_name}${resource.subject_kind ? ` · ${SUBJECT_LABEL[resource.subject_kind]}` : ' · تجهیزات'}`}
|
description={`${resource.type_name}${resource.subject_kind ? ` · ${SUBJECT_LABEL[resource.subject_kind]}` : ' · تجهیزات'}`}
|
||||||
backTo="/admin/resources"
|
backTo="/admin/resources"
|
||||||
|
tourId="resource-detail"
|
||||||
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: resource.name }]}
|
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: resource.name }]}
|
||||||
action={
|
action={
|
||||||
canUpdate ? (
|
canUpdate ? (
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export default function ResourcePoolsPage() {
|
|||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="استخر منابع"
|
title="استخر منابع"
|
||||||
|
tourId="resource-pools"
|
||||||
description="منابعی که جایگزین کامل یکدیگرند — مثل «لیزرهای آلکساندرایت». همهٔ اعضا باید از یک نوع باشند."
|
description="منابعی که جایگزین کامل یکدیگرند — مثل «لیزرهای آلکساندرایت». همهٔ اعضا باید از یک نوع باشند."
|
||||||
backTo="/admin/resources"
|
backTo="/admin/resources"
|
||||||
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: 'استخر منابع' }]}
|
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: 'استخر منابع' }]}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export default function ResourceTypesPage() {
|
|||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="نوع منابع"
|
title="نوع منابع"
|
||||||
|
tourId="resource-types"
|
||||||
description="دستهبندی منابع: پزشک، پرسنل، اتاق، دستگاه لیزر، یونیت. نوعهای سیستمی حذف نمیشوند چون پل خودکار منابع به آنها تکیه دارد."
|
description="دستهبندی منابع: پزشک، پرسنل، اتاق، دستگاه لیزر، یونیت. نوعهای سیستمی حذف نمیشوند چون پل خودکار منابع به آنها تکیه دارد."
|
||||||
backTo="/admin/resources"
|
backTo="/admin/resources"
|
||||||
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: 'نوع منابع' }]}
|
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: 'نوع منابع' }]}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export default function ResourcesPage() {
|
|||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="منابع"
|
title="منابع"
|
||||||
|
tourId="resources"
|
||||||
description="هر چیزی که ممکن است اشغال باشد: پزشک، اپراتور، اتاق، دستگاه. ظرفیت یعنی تعداد بیمار همزمان."
|
description="هر چیزی که ممکن است اشغال باشد: پزشک، اپراتور، اتاق، دستگاه. ظرفیت یعنی تعداد بیمار همزمان."
|
||||||
/* سقف پلن پیش از باز شدن فرم گفته میشود، نه بعد از پر کردنش: خطای ۴۲۲ ته کار
|
/* سقف پلن پیش از باز شدن فرم گفته میشود، نه بعد از پر کردنش: خطای ۴۲۲ ته کار
|
||||||
همان اطلاعات را دیرتر و گرانتر میداد. تصمیم نهایی همچنان با سرور است. */
|
همان اطلاعات را دیرتر و گرانتر میداد. تصمیم نهایی همچنان با سرور است. */
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export default function SecretaryEarningsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<PageHeader title="درآمد نوبتهای آنلاین" description="سهم شما از نوبتهایی که آنلاین ثبت و پرداخت شدهاند" />
|
<PageHeader title="درآمد نوبتهای آنلاین" description="سهم شما از نوبتهایی که آنلاین ثبت و پرداخت شدهاند"tourId="secretary-earnings" />
|
||||||
|
|
||||||
{summary && !summary.enabled ? (
|
{summary && !summary.enabled ? (
|
||||||
<div className="card" style={{ padding: 20, fontSize: 13, color: 'var(--text-2)', lineHeight: 1.9 }}>
|
<div className="card" style={{ padding: 20, fontSize: 13, color: 'var(--text-2)', lineHeight: 1.9 }}>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export default function SecretarySettlementPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fade-in">
|
<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)' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 'var(--gap)', marginBottom: 'var(--gap)' }}>
|
||||||
<StatCard label="موجودی قابل برداشت" value={formatRial(balance)} tone="green" />
|
<StatCard label="موجودی قابل برداشت" value={formatRial(balance)} tone="green" />
|
||||||
|
|||||||
@@ -436,6 +436,7 @@ function ServiceDetailPageInner() {
|
|||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
backTo="/admin/clinic-services"
|
backTo="/admin/clinic-services"
|
||||||
|
tourId="service-detail"
|
||||||
title={item.name}
|
title={item.name}
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
{ label: 'سرویسها', to: '/admin/clinic-services' },
|
{ label: 'سرویسها', to: '/admin/clinic-services' },
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import PaymentStep from '../components/session/PaymentStep';
|
|||||||
import DetailsStep from '../components/session/DetailsStep';
|
import DetailsStep from '../components/session/DetailsStep';
|
||||||
import { CloseModalD } from '../components/icons/FilesServiceIcons';
|
import { CloseModalD } from '../components/icons/FilesServiceIcons';
|
||||||
import { Breadcrumb } from '../components/PatientCaseBanner';
|
import { Breadcrumb } from '../components/PatientCaseBanner';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* تکمیل پرداخت مراجعه — پورت صفحهی tauri /files/create-service در حالت payment:
|
* تکمیل پرداخت مراجعه — پورت صفحهی tauri /files/create-service در حالت payment:
|
||||||
@@ -56,11 +57,14 @@ export default function SessionPaymentPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fade-in" style={{ width: '100%' }}>
|
<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 */}
|
{/* card — tauri width 748 centered */}
|
||||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
|
<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 }}>
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ChevronLeftIcon } from '@heroicons/react/24/outline';
|
|||||||
import { groupedMenuForRole } from '../components/layout/settingsMenu';
|
import { groupedMenuForRole } from '../components/layout/settingsMenu';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SettingsMenuPage — the settings landing list for doctor/clinic users.
|
* SettingsMenuPage — the settings landing list for doctor/clinic users.
|
||||||
@@ -22,7 +23,7 @@ export default function SettingsMenuPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fade-in" style={{ maxWidth: 720, margin: '0 auto' }}>
|
<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) => (
|
{groups.map((group) => (
|
||||||
<section key={group.key} aria-label={group.label} style={{ marginBottom: 20 }}>
|
<section key={group.key} aria-label={group.label} style={{ marginBottom: 20 }}>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export default function SkillsPage() {
|
|||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="مهارتها"
|
title="مهارتها"
|
||||||
|
tourId="resource-skills"
|
||||||
description="مهارت روی منابع مینشیند و در انتخاب منبع مناسب استفاده میشود. مهارتی که به منبعی داده شده، تا برداشته نشود حذف نمیشود."
|
description="مهارت روی منابع مینشیند و در انتخاب منبع مناسب استفاده میشود. مهارتی که به منبعی داده شده، تا برداشته نشود حذف نمیشود."
|
||||||
backTo="/admin/resources"
|
backTo="/admin/resources"
|
||||||
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: 'مهارتها' }]}
|
breadcrumbs={[{ label: 'منابع', to: '/admin/resources' }, { label: 'مهارتها' }]}
|
||||||
|
|||||||
@@ -117,10 +117,10 @@ function SmsWalletPageInner() {
|
|||||||
|
|
||||||
return (
|
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 */}
|
{/* کارت موجودی — gradient */}
|
||||||
<div style={{
|
<div style={{
|
||||||
background: 'linear-gradient(135deg, oklch(0.52 0.22 256), oklch(0.40 0.18 256))',
|
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">
|
<SettingsLayout active="staff">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="مدیریت پرسنل"
|
title="مدیریت پرسنل"
|
||||||
|
tourId="staff"
|
||||||
description="لیست پرسنل کلینیک / مطب"
|
description="لیست پرسنل کلینیک / مطب"
|
||||||
action={
|
action={
|
||||||
canCreate ? (
|
canCreate ? (
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export default function StaffSessionDetailPage() {
|
|||||||
<>
|
<>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={`جلسهٔ ${formatNumber(session.session_number)} از ${formatNumber(session.total_sessions)}`}
|
title={`جلسهٔ ${formatNumber(session.session_number)} از ${formatNumber(session.total_sessions)}`}
|
||||||
|
tourId="my-session-detail"
|
||||||
backTo="/admin/my-sessions"
|
backTo="/admin/my-sessions"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export default function StaffTreatmentSessionsPage() {
|
|||||||
<>
|
<>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="جلسات امروز من"
|
title="جلسات امروز من"
|
||||||
|
tourId="my-sessions"
|
||||||
action={
|
action={
|
||||||
<button type="button" className="btn secondary sm" onClick={() => refetch()} disabled={isFetching}>
|
<button type="button" className="btn secondary sm" onClick={() => refetch()} disabled={isFetching}>
|
||||||
{isFetching ? 'در حال بهروزرسانی…' : 'بهروزرسانی'}
|
{isFetching ? 'در حال بهروزرسانی…' : 'بهروزرسانی'}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { usePaymentConfig } from '../hooks/usePaymentConfig';
|
|||||||
import { formatRial, formatNumber, formatDate, formatResourceLimit } from '../lib/utils';
|
import { formatRial, formatNumber, formatDate, formatResourceLimit } from '../lib/utils';
|
||||||
import Modal from '../components/ui/Modal';
|
import Modal from '../components/ui/Modal';
|
||||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
import SettingsLayout from '../components/layout/SettingsLayout';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
import {
|
import {
|
||||||
SparkleIcon, PlanCardPerson, CloseCircleFilled, GiftIcon, InfoCircleRedFilled,
|
SparkleIcon, PlanCardPerson, CloseCircleFilled, GiftIcon, InfoCircleRedFilled,
|
||||||
} from './subscriptionIcons';
|
} from './subscriptionIcons';
|
||||||
@@ -135,9 +136,12 @@ export default function SubscriptionPage() {
|
|||||||
<div dir="ltr" style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 32, paddingTop: 8 }}>
|
<div dir="ltr" style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 32, paddingTop: 8 }}>
|
||||||
{/* ── Header: current plan (left) + title (right) ── */}
|
{/* ── Header: current plan (left) + title (right) ── */}
|
||||||
<div style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center', gap: 16, flexWrap: 'wrap' }}>
|
<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>
|
||||||
<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 style={{ fontSize: 14, fontWeight: 500, color: 'var(--text-3)', marginTop: 4, textAlign: 'left' }}>
|
||||||
پلن اشتراک مناسب خود را انتخاب نمایید:
|
پلن اشتراک مناسب خود را انتخاب نمایید:
|
||||||
</div>
|
</div>
|
||||||
@@ -156,7 +160,7 @@ export default function SubscriptionPage() {
|
|||||||
در حال حاضر پلنی برای نمایش وجود ندارد.
|
در حال حاضر پلنی برای نمایش وجود ندارد.
|
||||||
</div>
|
</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) => (
|
{plans.map((plan) => (
|
||||||
<PlanCard
|
<PlanCard
|
||||||
key={plan.uuid}
|
key={plan.uuid}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import ConfirmDialog from '../components/ui/ConfirmDialog';
|
|||||||
import SettingsLayout from '../components/layout/SettingsLayout';
|
import SettingsLayout from '../components/layout/SettingsLayout';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
import Switch from '../components/ui/Switch';
|
import Switch from '../components/ui/Switch';
|
||||||
|
import TourButton from '../components/ui/TourButton';
|
||||||
|
|
||||||
interface TenantTag { uuid: string; name: string; color: string; active: boolean }
|
interface TenantTag { uuid: string; name: string; color: string; active: boolean }
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ export default function TagsSettingsPage() {
|
|||||||
<SettingsLayout active="tags">
|
<SettingsLayout active="tags">
|
||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16, gap: 12, flexWrap: 'wrap' }}>
|
<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 && (
|
{canCreate && (
|
||||||
<button className="btn primary" onClick={openCreate}><PlusIcon style={{ width: 16 }} /> برچسب جدید</button>
|
<button className="btn primary" onClick={openCreate}><PlusIcon style={{ width: 16 }} /> برچسب جدید</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -58,9 +58,9 @@ export default function TreatmentCasesPage() {
|
|||||||
|
|
||||||
return (
|
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) => (
|
{TABS.map((t) => (
|
||||||
<button
|
<button
|
||||||
key={t.id}
|
key={t.id}
|
||||||
|
|||||||
@@ -200,6 +200,14 @@ Each tour carries a `version` in the frontend registry (`assets/admin/lib/tour/t
|
|||||||
The tour auto-runs again when its version is higher than the stored one, which is how a rewritten
|
The tour auto-runs again when its version is higher than the stored one, which is how a rewritten
|
||||||
tour reaches users who already saw the old text.
|
tour reaches users who already saw the old text.
|
||||||
|
|
||||||
|
Every non-admin page of the panel ships a tour. Adding one to a new page means: a file under
|
||||||
|
`assets/admin/lib/tour/tours/`, one line in `registry.ts`, and `tourId` on the page's `PageHeader`
|
||||||
|
— the shared `data-tour` anchors (`page-title`, `page-action`, `page-search`, `page-table`,
|
||||||
|
`page-pagination`) already come from `PageHeader`, `DataTable` and `Pagination`. A step whose
|
||||||
|
anchor is not on screen is dropped silently, so role-specific pages need no branching.
|
||||||
|
`PageHeader` auto-runs the tour once the page is ready; pass `tourReady={!isLoading}` when the
|
||||||
|
data arrives late.
|
||||||
|
|
||||||
## GET `/api/v1/my/tours`
|
## GET `/api/v1/my/tours`
|
||||||
|
|
||||||
Tours the current user has already been through.
|
Tours the current user has already been through.
|
||||||
|
|||||||
Reference in New Issue
Block a user