+```
+
+### utility روشن بدون جفت dark — `pages/AppointmentDetailPage.tsx`
+
+```tsx
+// خط ۱۴۱–۱۴۲
+
+
اطلاعات بیمار
+```
+
+### utility روشن بدون جفت dark — `pages/RepresentationDetailPage.tsx`
+
+```tsx
+// خط ۲۵۱
+className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-[10px] border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors"
+// خط ۳۰۹
+
اطلاعات بانکی ثبت نشده
+```
+
+### CSS ثابت — `assets/admin/styles.css` خط ۹۵۸–۹۷۰
+
+```css
+.inv-table { width: 100%; border-collapse: collapse; }
+.inv-table thead tr { background: #e1e1e1; } /* بدون override دارک */
+.inv-table th {
+ color: #616161; font-size: 14px; font-weight: 400; /* بدون override دارک */
+ padding: 10px 18px; text-align: start; white-space: nowrap;
+}
+.inv-table td {
+ padding: 12px 18px; text-align: start; white-space: nowrap;
+ font-size: 16px; font-weight: 500; color: var(--text-2);
+ border-bottom: 1px solid #DBDBDB;
+}
+.dark .inv-table td { border-color: var(--border); }
+.inv-table tbody tr:hover { background: #f4f5fd; }
+.dark .inv-table tbody tr:hover { background: var(--surface-2); }
+```
+
+### الگوی درستِ موجود در پروژه (مرجع سبک) — `components/FreeVisitPrice.tsx`
+
+```tsx
+
+ *
+```
+
+این همان الگویی است که باید همهجا اعمال شود: inline style میماند، فقط مقدار رنگ به توکن تبدیل میشود.
+
+## وظایف
+
+### ۱. تثبیت جدول نگاشت رنگ → توکن
+
+قبل از هر تغییر، این جدول را مبنا بگیر. hexها از شمارش واقعی فایلهای `.tsx` استخراج شدهاند (ستون «تعداد» = تکرار در سورس):
+
+| hex فعلی (لایت) | تعداد | hex فعلی (دارک، در `dark:`ها) | توکن مقصد |
+|---|---|---|---|
+| `#525252`, `#3b3b3b`, `#2f2f2f`, `#111827` | ۴۵+۱۳+۱۶+۱۱ | `#d7d8ed` (۷۶) | `var(--text)` |
+| `#616161`, `#7e7e7e`, `#6b7280` | ۵۱+۱۹+۳۱ | `#a1a1a1` (۵۰) | `var(--text-2)` |
+| `#9ca3af` | ۱۰ | — | `var(--text-3)` |
+| `#5559ce` | ۷۵ | — | `var(--primary)` |
+| `#f17732`, `#f0753b` | ۳۴+۴ | — | `var(--accent)` |
+| `#ffffff` (سطح کارت) | ۴ | `#222433` (۲۵) | `var(--surface)` |
+| `#fafafa`, `#f1f1f1` (پسزمینهٔ صفحه/فیلد) | ۶+۴ | — | `var(--bg)` / `var(--surface-2)` |
+| `#efefef`, `#ededed`, `#e7e7e7` | ۱۸+۴+۴ | `#35343d` (۲۲), `#343645` (۱۱) | `var(--border)` |
+| `#e0e0e0`, `#e1e1e1`, `#dbdbdb`, `#d7d7d7` | ۱۴+۸+… | `#404040` (۱۴) | `var(--border-2)` |
+| `#ef4444`, `#d32f2f` | ۱۰+۹ | — | `var(--danger)` |
+| `#2e7d32`, `#3c9a4f` | ۸+۷ | — | `var(--success)` |
+| `#f59e0b` | ۵ | — | `var(--warning)` |
+| پسزمینهٔ نرم بنفش `#f7f8ff`, `#f4f5fd`, `#e8ebff` | — | — | `var(--primary-soft)` / `var(--primary-soft2)` |
+
+**نکته:** بعضی نگاشتها دقیقاً یکسان نیستند (`#525252` در برابر `--text: #3b3b3b`). این اختلاف عمدی پذیرفته میشود چون هدف یکدستسازی است؛ اما اگر جایی اختلاف بصری محسوس شد (مثلاً متن اصلی کارت که واضحاً تیرهتر/روشنتر میشود)، **همانجا را در گزارش پایانی لیست کن** بهجای اینکه توکن جدید بسازی.
+
+**نحوه تست:** جدول تصمیم است، نه کد. بعد از تعریف، در `docs/admin-ui/ui-design-spec.md` ثبتش کن (وظیفهٔ ۷).
+
+### ۲. پاکسازی `dark:`های مرده (دستهٔ A، اولویت اول)
+
+در ۱۰ فایلی که در بخش «مشکل» لیست شد: هر جا `className` شامل `dark:` کنار `style={{ color|background|border|borderColor: '#…' }}` است:
+
+1. کلاس `dark:` مرده را **حذف** کن
+2. مقدار hex داخل `style` را طبق جدول وظیفهٔ ۱ به `var(--token)` تبدیل کن
+
+```tsx
+// قبل
+
+
+// بعد
+
+```
+
+```tsx
+// قبل
+
+
+// بعد
+
+```
+
+ثابتهای ماژولسطح (`fieldLabel`, `primaryBtn`, `ghostBtn` در `PaymentStep.tsx`) هم همینطور:
+
+```tsx
+const fieldLabel: React.CSSProperties = { fontSize: 14, color: 'var(--text-2)', marginBottom: 8, display: 'block' };
+const primaryBtn: React.CSSProperties = { background: 'var(--primary)', color: 'var(--on-primary)', ... };
+const ghostBtn: React.CSSProperties = { background: 'transparent', color: 'var(--primary)', border: '1px solid var(--primary)', ... };
+```
+
+**نحوه تست:**
+- `ddev exec npm run test` سبز بماند (تستهای موجود `PaymentStep`/`SessionPaymentAccordion`/… نباید بشکنند)
+- تأیید صفر شدن الگو:
+ `grep -rn "dark:" --include='*.tsx' assets/admin | grep -E "style=\{\{[^}]*(color|background)" | grep -v '\.test\.' | wc -l` → باید `0` شود
+- بصری: `/admin` → جلسه (session) → مراحل ایجاد/جزئیات/پرداخت، یکبار لایت و یکبار دارک
+
+### ۳. تبدیل بقیهٔ inline styleهای رنگی به توکن (دستهٔ A، ادامه)
+
+فایلبهفایل، به ترتیب حجم (MyPatientsPage → PatientDetailPage → SmsWalletPage → SubscriptionPage → ClinicServicesPage → PatientsListPage → MySecretariesPage → DoctorFormPage → DashboardPage → AdminSubscriptionPage → TurnsTimeline → …).
+
+قواعد:
+- رنگ ثابت داخل `style` → `var(--token)`
+- کلاسهای arbitrary دوتایی مثل `className="text-[#525252] dark:text-[#D7D8ED]"` → `className="text-[var(--text)]"` (یک کلاس، هر دو مود)
+- `bg-[#FAFAFA] dark:bg-[#222433]` → `bg-[var(--surface)]` یا `bg-[var(--bg)]` بسته به اینکه سطح کارت است یا پسزمینهٔ فیلد
+- استثنا: hex داخل SVG که با prop رنگ میگیرد (مثل `
`) — در وظیفهٔ ۶
+- **هر فایل یک commit/گام مستقل**؛ فایل بعدی را قبل از تأیید بصری فایل قبلی شروع نکن
+
+**نحوه تست:** بعد از هر فایل: `ddev exec npm run test`، سپس بازکردن صفحهٔ متناظر در `/admin` در هر دو مود و مقایسه با اسکرینشات قبلِ تغییر (لایت باید بدون تغییر باشد).
+
+### ۴. اضافه کردن پوشش دارک به دستهٔ B
+
+در `pages/RepresentationDetailPage.tsx`، `pages/AppointmentDetailPage.tsx`، `pages/PaymentDetailPage.tsx`، `components/ImageCropModal.tsx`:
+
+utilityهای پالت ثابت را با کلاس توکنی جایگزین کن — نه اینکه جفت `dark:` اضافه کنی (جفتسازی بدهی را دو برابر میکند و با brand hue هم هماهنگ نمیشود):
+
+```tsx
+// قبل
+
+
اطلاعات بیمار
+
+// بعد
+
+
اطلاعات بیمار
+```
+
+```tsx
+// قبل (RepresentationDetailPage خط ۲۵۱)
+className="… border border-gray-300 text-gray-700 hover:bg-gray-50 …"
+// بعد
+className="… border border-[var(--border-2)] text-[var(--text)] hover:bg-[var(--surface-2)] …"
+```
+
+برای رنگهای وضعیتی (`text-red-600`, `border-red-300`, `bg-red-50`) → `text-[var(--danger)]`, `border-[var(--danger)]/30`, `bg-[var(--danger-bg)]`.
+
+**نحوه تست:** `/admin/representations/{id}`، `/admin/appointments/{id}`، `/admin/payments/{id}` و مودال کراپ تصویر (آپلود آواتار پزشک) — هر کدام در دارک و لایت.
+
+### ۵. پاکسازی `styles.css`
+
+- `.inv-table thead tr` → `background: var(--surface-2)`
+- `.inv-table th` → `color: var(--text-2)`
+- `.inv-table td` → `border-bottom: 1px solid var(--border)` و حذف `.dark .inv-table td { border-color: … }` که دیگر لازم نیست
+- `.inv-table tbody tr:hover` → `background: var(--surface-2)` و حذف override `.dark`
+- `.inv-tabs` → `border-bottom: 1px solid var(--border)` و حذف `.dark .inv-tabs`
+- `.inv-tab` → `color: var(--text-2)`؛ `.inv-tab.active` → `color: var(--primary); border-bottom: 3px solid var(--primary)`؛ حذف هر دو override `.dark`
+- `.inv-badge.in-stock|low-stock|out-of-stock` → `background: var(--success-bg)|var(--warning-bg)|var(--danger-bg)` و `color: var(--success)|var(--warning)|var(--danger)`؛ حذف سه override `.dark`
+
+`color: #fff` روی `.btn`/`.avatar` (خطوط ۲۶۳، ۴۱۱، ۵۹۸، ۶۷۱، ۷۷۵) → `var(--on-primary)` هر جا روی پسزمینهٔ برند است؛ `rgba(8,13,22,.5)` اسکریم مودال (خطوط ۶۹۶، ۷۹۸) عمداً در هر دو مود یکسان است و **دست نمیخورد** — در گزارش ذکرش کن.
+
+**نحوه تست:** `/admin` → انبار (inventory): جدول، تبها و badgeهای موجودی در هر دو مود؛ `ddev exec npm run build` بدون خطا.
+
+### ۶. آیکونهای SVG
+
+آیکونهای inline با `stroke="#616161"` / `#3B3B3B` / `fill="#7E7E7E"` (مثلاً `pages/MySecretariesPage.tsx` خطوط ۲۶–۳۵ و فایلهای `components/icons/`) را به `stroke="currentColor"` تبدیل کن و رنگ را از کلاس والد بگیر (`text-[var(--text-2)]`).
+
+استثنا: آیکونهای چندرنگ برند (`stroke="#F17732"` در `components/icons/FilesServiceIcons.tsx`، ۲۲ مورد) — اینها هویت رنگی دارند؛ به `var(--accent)` تبدیل کن، نه `currentColor`.
+
+```tsx
+// قبل
+
+// بعد — رنگ از والد
+
+
+
+```
+
+**نحوه تست:** صفحهٔ «منشیهای من» و «سرویسها» در دارکمود — آیکونها باید دیده شوند نه اینکه در پسزمینه گم شوند.
+
+### ۷. تست گارد + بهروزرسانی سند
+
+**۷.۱ تست گارد** — `assets/admin/test/theme-tokens.test.ts` (Vitest، همان setup موجود در `assets/admin/test/`):
+
+```ts
+// اسکن سورس؛ فایلهای پاکشده در فاز جاری در allowlist نیستند و باید تمیز بمانند.
+const CLEANED = [ /* مسیر فایلهایی که در وظایف ۲–۴ پاک شدند */ ];
+
+it('فایلهای پاکشده hex ثابت رنگی ندارند', () => { /* regex #rrggbb داخل style/className، بهجز SVG برند */ });
+it('الگوی مردهٔ dark: کنار inline style رنگی وجود ندارد', () => { /* کل assets/admin */ });
+```
+
+تست دوم باید روی **کل** `assets/admin` اجرا شود (نه فقط allowlist) چون بعد از وظیفهٔ ۲ باید صفر باشد و هرگز برنگردد.
+
+**۷.۲ سند** — `docs/admin-ui/ui-design-spec.md`:
+- بخش ۱۶ فعلی («Dark Mode (اختیاری — فاز دوم)» با `.dark { }` و پالت Tailwind) با پیادهسازی واقعی نمیخواند → بازنویسی شود: دارکمود پیادهشده است، از طریق `data-theme`/کلاس `dark` روی `` توسط `uiStore.applyTheme()`، و منبع رنگ **فقط** توکنهای `styles.css` است.
+- جدول نگاشت وظیفهٔ ۱ + قاعدهٔ الزامی: «رنگ ثابت (hex یا utility پالت Tailwind) در `assets/admin` ممنوع؛ `var(--token)` یا `*-[var(--token)]`».
+- بخش ۱ (Color Palette) که هنوز `--color-bg-sidebar: #0f172a` دارد با توکنهای واقعی همخوان شود.
+
+**نحوه تست:** `ddev exec npm run test` — گارد جدید سبز؛ برای اطمینان از اینکه واقعاً گارد است، یکبار موقتاً `color: '#525252'` به یکی از فایلهای allowlist اضافه کن، ببین تست قرمز میشود، بعد برگردان.
+
+### ۸. (فاز ۳ — فقط با تأیید صریح کاربر) مهاجرت سه فایل بزرگ
+
+`pages/DoctorDetailPage.tsx` (۳۱۳ utility رنگی / ۱۴۲ `dark:`)، `components/schedule/ScheduleSection.tsx` (۲۶۰ / ۱۲۰)، `pages/UserDetailPage.tsx` (۱۹۸ / ۹۱).
+
+اینها **در دارکمود کار میکنند** (جفت `slate/gray` + `dark:` دارند) ولی از design-system منحرفاند: با تغییر brand hue هماهنگ نمیشوند و رنگ خاکستریشان با `--surface`/`--text` مو نمیزند. مهاجرت ۷۷۱ نقطه ریسک رگرسیون بصری بالایی دارد و سود عملکردی فوری ندارد.
+
+**توصیه:** این وظیفه را در همین پاس انجام نده. اگر کاربر تأیید کرد، فایلبهفایل و با اسکرینشات قبل/بعد در هر دو مود.
+
+## نکات مهم
+
+- **علت ریشهای، نه علامت:** الگوی `className="dark:…"` کنار `style={{color:…}}` نشان میدهد قبلاً تلاش شده دارکمود «وصله» شود بدون اینکه specificity در نظر گرفته شود. راهحل درست حذف رنگ ثابت است، نه اضافه کردن `!important` یا جفت `dark:` بیشتر. اگر جایی واقعاً به override نیاز شد، اول ببین آیا میشود کلاً از `style` به `className` رفت.
+- **ترتیب اجرا اجباری است:** وظیفهٔ ۲ (خرابی قطعی، ۹۰ نقطه) قبل از وظیفهٔ ۳ (حجیم). اگر بودجه/زمان تمام شد، وظایف ۲، ۴، ۵ حداقل قابل تحویل مستقلاند.
+- **لایتمود نباید تغییر کند.** توکنهای لایت همان hexهای رایجاند؛ هر تغییر بصری محسوس در لایت یعنی نگاشت اشتباه انتخاب شده — گزارش کن، خودسرانه توکن جدید نساز.
+- **توکن جدید فقط با دلیل.** اگر جایی هیچ توکن مناسبی نبود، اول بررسی کن آیا واقعاً رنگ جدید لازم است یا نزدیکترین توکن کافی است (guidelines §۵ — abstraction بیمصرف ممنوع). توکن جدید اگر اضافه شد باید در **هر سه** بلوک `:root`، `[data-theme="dark"]` و در صورت لزوم `@supports` تعریف شود.
+- **سازگاری مرورگر قدیمی:** بلوک `@supports (color: color-mix(in oklch, red, blue))` عمداً fallback sRGB دارد (iPhone 8 / WebKit قدیمی). هیچ توکنی نباید **فقط** داخل `@supports` تعریف شود.
+- **`brandHue.fixed`:** هیوی «نارنجی» در `uiStore.ts` مقادیر `--primary*` را inline روی root میگذارد. یعنی هر جا `#5559CE` ثابت مانده باشد با انتخاب این هیو ناهماهنگ میشود — این یک تست بصری مستقل است، نه فقط دارک/لایت.
+- **بدون تغییر backend/API.** این تسک صفر تغییر در `src/` و `docs/api/` دارد؛ تنها سند متأثر `docs/admin-ui/ui-design-spec.md` است.
+- **دستورها داخل ddev اجرا میشوند:** `ddev exec npm run test`، `ddev exec npm run build`. برای مشاهدهٔ زنده `ddev exec npm run watch`.
+- **حساب تست:** `09390039833 / 09390039833` روی `https://clinic-pro.ddev.site/admin`.
diff --git a/assets/admin/components/AppointmentTurnCard.tsx b/assets/admin/components/AppointmentTurnCard.tsx
index d8bfa328..f3ebe1d5 100644
--- a/assets/admin/components/AppointmentTurnCard.tsx
+++ b/assets/admin/components/AppointmentTurnCard.tsx
@@ -26,13 +26,13 @@ function InfoRow({ icon, label, value, chip = false, valueStyle }: {
{icon}
- {label}:
+ {label}:
@@ -55,7 +55,7 @@ export default function AppointmentTurnCard({ appointment, queryKey }: {
const title = appointment.service_name || 'نوبت';
return (
{/* Header */}
@@ -63,13 +63,13 @@ export default function AppointmentTurnCard({ appointment, queryKey }: {
-
+
-
+
{/* Middle: date & time chips */}
@@ -77,7 +77,7 @@ export default function AppointmentTurnCard({ appointment, queryKey }: {
} label="ساعت" chip value={formatTime(appointment.starts_at)} />
-
+
{/* Bottom: personnel & status */}
diff --git a/assets/admin/components/FreeVisitPrice.tsx b/assets/admin/components/FreeVisitPrice.tsx
index 5c532182..94262e80 100644
--- a/assets/admin/components/FreeVisitPrice.tsx
+++ b/assets/admin/components/FreeVisitPrice.tsx
@@ -79,7 +79,7 @@ export default function FreeVisitPrice({ doctorUuid, readOnly = false }: { docto
الزامی کردن هزینه ویزیت
diff --git a/assets/admin/components/ImageCropModal.tsx b/assets/admin/components/ImageCropModal.tsx
index eed4ea11..68725277 100644
--- a/assets/admin/components/ImageCropModal.tsx
+++ b/assets/admin/components/ImageCropModal.tsx
@@ -31,14 +31,14 @@ export default function ImageCropModal({ src, fileName, onCancel, onConfirm }: I
return createPortal(
-
-
-
برش تصویر
+
+
+
برش تصویر
×
+ className="text-[var(--text-3)] hover:text-[var(--text-2)] text-xl leading-none">×
-
+
- بزرگنمایی
+ بزرگنمایی
setZoom(Number(e.target.value))}
className="w-full accent-indigo-600" />
+ className="px-4 py-2 rounded-xl text-sm font-medium text-[var(--text-2)] bg-[var(--surface-2)] hover:bg-[var(--surface-3)] dark:hover:bg-[var(--surface-2)] disabled:opacity-50">
انصراف
+ className="px-4 py-2 rounded-xl text-sm font-medium text-[var(--on-primary)] bg-[var(--primary-600)] hover:bg-[var(--primary-700)] disabled:opacity-50">
{processing ? 'در حال برش...' : 'ذخیره'}
diff --git a/assets/admin/components/InvoiceSummaryModal.tsx b/assets/admin/components/InvoiceSummaryModal.tsx
index d9f8b3a1..985c25c4 100644
--- a/assets/admin/components/InvoiceSummaryModal.tsx
+++ b/assets/admin/components/InvoiceSummaryModal.tsx
@@ -103,7 +103,7 @@ export default function InvoiceSummaryModal({ invoiceUuid, onClose }: { invoiceU
rows={[[
formatDate(session?.session_at ?? inv.issued_at),
session?.paid_at ? formatDate(session.paid_at) : paid ? formatDate(inv.issued_at) : '—',
- 0 ? '#d32f2f' : '#388e3c', fontWeight: 600 }}>{statusLabel} ,
+ 0 ? 'var(--danger)' : 'var(--success)', fontWeight: 600 }}>{statusLabel} ,
]]}
/>
{(inv.base_insurance_name || inv.service_category_label) && (
@@ -168,7 +168,7 @@ export default function InvoiceSummaryModal({ invoiceUuid, onClose }: { invoiceU
cols={['مبلغ کل پرداخت شده', 'مبلغ باقی مانده']}
rows={[[
formatRial(paidAmount),
- 0 ? '#d32f2f' : '#388e3c', fontWeight: 600 }}>{formatRial(remaining)} ,
+ 0 ? 'var(--danger)' : 'var(--success)', fontWeight: 600 }}>{formatRial(remaining)} ,
]]}
/>
diff --git a/assets/admin/components/NewAppointmentDrawer.tsx b/assets/admin/components/NewAppointmentDrawer.tsx
index f457618c..5744ef38 100644
--- a/assets/admin/components/NewAppointmentDrawer.tsx
+++ b/assets/admin/components/NewAppointmentDrawer.tsx
@@ -309,7 +309,7 @@ export default function NewAppointmentDrawer({ doctorUuid, defaultDate, queryKey
setPickedSlot({ start: s.start, end: s.end })}
style={{ fontSize: 13, padding: '6px 12px', borderRadius: 'var(--r-sm)', cursor: 'pointer', fontFamily: 'inherit',
border: active ? '1px solid var(--primary)' : '1px solid var(--border)',
- background: active ? 'var(--primary)' : 'var(--surface)', color: active ? '#fff' : 'var(--text)' }}>
+ background: active ? 'var(--primary)' : 'var(--surface)', color: active ? 'var(--on-primary)' : 'var(--text)' }}>
{s.start_time}
);
diff --git a/assets/admin/components/PatientCaseBanner.tsx b/assets/admin/components/PatientCaseBanner.tsx
index 8de8703e..e0b146e8 100644
--- a/assets/admin/components/PatientCaseBanner.tsx
+++ b/assets/admin/components/PatientCaseBanner.tsx
@@ -10,15 +10,15 @@ interface Tag { uuid: string; name: string; color: string }
/** پرونده > {name} breadcrumb, ported from tauri BreadcrumbHeader. */
export function Breadcrumb({ name, backTo }: { name: string; backTo: string }) {
return (
-
-
+
+
بازگشت
پرونده
-
{name}
+
{name}
);
}
@@ -37,8 +37,8 @@ function TagDots({ tags }: { tags?: Tag[] }) {
const InfoLine = ({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) => (
{icon}
- {label}
- {value}
+ {label}
+ {value}
);
@@ -60,21 +60,21 @@ export default function PatientCaseBanner({ name, recordNumber, mobile, createdA
const complete = !hasDebt;
return (
{/* right — name + tags */}
- {name}
+ {name}
{complete ? 'تکمیل شده' : 'تکمیل نشده'}
-
شماره پرونده: {recordNumber || '—'}
+
شماره پرونده: {recordNumber || '—'}
- برچسب ها:
+ برچسب ها:
@@ -90,7 +90,7 @@ export default function PatientCaseBanner({ name, recordNumber, mobile, createdA
} label="نوبت بعدی:" value={nextAppointment ? formatDate(nextAppointment) : '—'} />
یادداشت
diff --git a/assets/admin/components/PatientTagsCell.tsx b/assets/admin/components/PatientTagsCell.tsx
index 872c2b0f..d7bbdb55 100644
--- a/assets/admin/components/PatientTagsCell.tsx
+++ b/assets/admin/components/PatientTagsCell.tsx
@@ -87,7 +87,7 @@ export default function PatientTagsCell({ record }: { record: PatientRecord }) {
fontSize: 12, height: 24, padding: '0 8px', borderRadius: 999,
border: '1px solid var(--border)',
background: assigned ? tag.color : 'var(--surface-2)',
- color: assigned ? '#fff' : 'var(--text-2)',
+ color: assigned ? 'var(--on-primary)' : 'var(--text-2)',
}}
>
{assigned ?
:
}
diff --git a/assets/admin/components/PatientsFilterModal.tsx b/assets/admin/components/PatientsFilterModal.tsx
index 31248670..4af8c5b1 100644
--- a/assets/admin/components/PatientsFilterModal.tsx
+++ b/assets/admin/components/PatientsFilterModal.tsx
@@ -141,7 +141,7 @@ export default function PatientsFilterModal({ open, onClose, value, onApply }: {
style={{
fontSize: 12, height: 26, padding: '0 10px', borderRadius: 999, cursor: 'pointer',
border: '1px solid var(--border)',
- background: on ? t.color : 'var(--surface-2)', color: on ? '#fff' : 'var(--text-2)',
+ background: on ? t.color : 'var(--surface-2)', color: on ? 'var(--on-primary)' : 'var(--text-2)',
}}
>
{t.name}
diff --git a/assets/admin/components/SessionPaymentAccordion.tsx b/assets/admin/components/SessionPaymentAccordion.tsx
index ebb06b75..6f13621e 100644
--- a/assets/admin/components/SessionPaymentAccordion.tsx
+++ b/assets/admin/components/SessionPaymentAccordion.tsx
@@ -19,14 +19,14 @@ const PAYMENT_LABELS = PAYMENT_METHOD_LABELS;
/** vertical hairline divider between meta columns (tauri MUI vertical Divider). */
function VDivider({ h = 24 }: { h?: number }) {
- return
;
+ return
;
}
function Meta({ label, value }: { label: string; value: string }) {
return (
- {label}:
- {value}
+ {label}:
+ {value}
);
}
@@ -49,16 +49,16 @@ export default function SessionPaymentAccordion({ session, expanded, onToggle }:
const finalPrice = formatRial(session.final_price_rials ?? 0);
return (
-
+
{/* summary */}
- {name}
+ {name}
@@ -66,26 +66,26 @@ export default function SessionPaymentAccordion({ session, expanded, onToggle }:
{paid ? 'پرداخت شده' : 'تسویه نشده'}
-
+
{/* details */}
{expanded && (
-
+
-
- پرداختیها
+
+ پرداختیها
{paid ? (
-
+
@@ -95,7 +95,7 @@ export default function SessionPaymentAccordion({ session, expanded, onToggle }:
) : (
-
هیچ پرداختی ثبت نشده است.
+
هیچ پرداختی ثبت نشده است.
)}
diff --git a/assets/admin/components/SessionServiceCard.tsx b/assets/admin/components/SessionServiceCard.tsx
index b40647ea..a2cf5515 100644
--- a/assets/admin/components/SessionServiceCard.tsx
+++ b/assets/admin/components/SessionServiceCard.tsx
@@ -55,8 +55,8 @@ export interface SessionCardData {
function Row({ label, value, valueStyle }: { label: string; value: string; valueStyle?: CSSProperties }) {
return (
- {label}:
- {value}
+ {label}:
+ {value}
);
}
@@ -99,27 +99,27 @@ export default function SessionServiceCard({ session, onSettle, onViewInvoice, o
return (
-
+
{title}
- {session.archived && آرشیو }
+ {session.archived && آرشیو }
- {subtitle}
+ {subtitle}
setMenuOpen((o) => !o)}
style={{ background: 'transparent', border: 'none', cursor: 'pointer', padding: 2, display: 'flex' }}>
-
+
{menuOpen && (
-
+
{ setMenuOpen(false); onViewInvoice(session); }}
style={{ display: 'block', width: '100%', textAlign: 'right', padding: '8px 10px', fontSize: 13, background: 'transparent', border: 'none', borderRadius: 6, cursor: 'pointer', color: 'var(--text)', fontFamily: 'inherit' }}>
مشاهده فاکتور
@@ -138,7 +138,7 @@ export default function SessionServiceCard({ session, onSettle, onViewInvoice, o
)}
{onArchive && (
{ setMenuOpen(false); onArchive(session, !session.archived); }}
- style={{ display: 'block', width: '100%', textAlign: 'right', padding: '8px 10px', fontSize: 13, background: 'transparent', border: 'none', borderRadius: 6, cursor: 'pointer', color: session.archived ? 'var(--text)' : '#EF4444', fontFamily: 'inherit' }}>
+ style={{ display: 'block', width: '100%', textAlign: 'right', padding: '8px 10px', fontSize: 13, background: 'transparent', border: 'none', borderRadius: 6, cursor: 'pointer', color: session.archived ? 'var(--text)' : 'var(--danger)', fontFamily: 'inherit' }}>
{session.archived ? 'خروج از آرشیو' : 'آرشیو'}
)}
@@ -147,7 +147,7 @@ export default function SessionServiceCard({ session, onSettle, onViewInvoice, o
-
+
@@ -155,7 +155,7 @@ export default function SessionServiceCard({ session, onSettle, onViewInvoice, o
-
+
{session.insurance_service_category_label && (
@@ -163,10 +163,10 @@ export default function SessionServiceCard({ session, onSettle, onViewInvoice, o
)}
{session.insurance_base_name &&
}
{(session.base_insurance_rials ?? 0) > 0 && (
-
+
)}
- {!paid &&
}
+ {!paid &&
}
@@ -175,7 +175,7 @@ export default function SessionServiceCard({ session, onSettle, onViewInvoice, o
type="button"
disabled={issuing}
onClick={() => onViewInvoice(session)}
- style={{ marginTop: 8, height: 40, width: '100%', borderRadius: 4, fontSize: 13, fontWeight: 500, cursor: 'pointer', color: '#4F46E5', background: 'transparent', border: '1px solid #5559ce' }}
+ style={{ marginTop: 8, height: 40, width: '100%', borderRadius: 4, fontSize: 13, fontWeight: 500, cursor: 'pointer', color: 'var(--primary)', background: 'transparent', border: '1px solid var(--primary)' }}
>
{issuing ? 'در حال صدور…' : 'مشاهده فاکتور'}
@@ -184,7 +184,7 @@ export default function SessionServiceCard({ session, onSettle, onViewInvoice, o
type="button"
disabled={settling}
onClick={() => onSettle(session.uuid)}
- style={{ marginTop: 8, height: 40, width: '100%', borderRadius: 4, fontSize: 13, fontWeight: 500, cursor: 'pointer', color: '#fff', background: '#5559ce', border: '1px solid #5559ce' }}
+ style={{ marginTop: 8, height: 40, width: '100%', borderRadius: 4, fontSize: 13, fontWeight: 500, cursor: 'pointer', color: 'var(--on-primary)', background: 'var(--primary)', border: '1px solid var(--primary)' }}
>
تکمیل پرداخت
diff --git a/assets/admin/components/SessionStepper.test.tsx b/assets/admin/components/SessionStepper.test.tsx
index 924e13cd..9c182e26 100644
--- a/assets/admin/components/SessionStepper.test.tsx
+++ b/assets/admin/components/SessionStepper.test.tsx
@@ -23,9 +23,9 @@ describe('SessionStepper', () => {
it('کانکتور گامهای طیشده بنفش و بقیه خاکستری', () => {
render(
);
- // jsdom رنگ hex را به rgb نرمال میکند
- expect(screen.getByTestId('step-connector-1').style.background).toContain('rgb(85, 89, 206)');
- expect(screen.getByTestId('step-connector-2').style.background).toContain('rgb(234, 234, 240)');
+ // رنگها از توکنهای تم میآیند تا در دارکمود هم درست باشند
+ expect(screen.getByTestId('step-connector-1').style.background).toContain('var(--primary)');
+ expect(screen.getByTestId('step-connector-2').style.background).toContain('var(--surface-2)');
});
it('استپر خالی: بدون کرش و بدون کانکتور', () => {
diff --git a/assets/admin/components/SessionStepper.tsx b/assets/admin/components/SessionStepper.tsx
index 6c01da12..5ba2d76c 100644
--- a/assets/admin/components/SessionStepper.tsx
+++ b/assets/admin/components/SessionStepper.tsx
@@ -21,8 +21,8 @@ function StepIcon({ index, active, completed }: { index: number; active: boolean
if (completed && !active) {
// دایره سفید با تیک سبز (tauri ColorlibStepIconRoot completed state)
return (
-
-
+
@@ -49,12 +49,12 @@ export default function SessionStepper({ activeStep = 0, steps = [] }: { activeS
style={{
position: 'absolute', top: 22, left: 'calc(50% + 40px)', right: 'calc(-50% + 40px)',
height: 3, borderRadius: 1,
- background: i <= activeStep ? 'linear-gradient(95deg, #5559ce 0%, #5559ce 100%)' : '#eaeaf0',
+ background: i <= activeStep ? 'linear-gradient(95deg, var(--primary) 0%, var(--primary) 100%)' : 'var(--surface-2)',
}}
/>
)}
-
+
{label}
diff --git a/assets/admin/components/TenantInsuranceContracts.tsx b/assets/admin/components/TenantInsuranceContracts.tsx
index b92e4989..d233a2e3 100644
--- a/assets/admin/components/TenantInsuranceContracts.tsx
+++ b/assets/admin/components/TenantInsuranceContracts.tsx
@@ -382,7 +382,7 @@ function StatusToggle({ contract, onToggle, disabled }: { contract: Contract; on
background: contract.is_active ? 'var(--primary)' : 'var(--border)',
}}>
diff --git a/assets/admin/components/appointments/ServiceSlotPicker.tsx b/assets/admin/components/appointments/ServiceSlotPicker.tsx
index 71131346..9249a2d1 100644
--- a/assets/admin/components/appointments/ServiceSlotPicker.tsx
+++ b/assets/admin/components/appointments/ServiceSlotPicker.tsx
@@ -130,7 +130,7 @@ export default function ServiceSlotPicker({
border: active ? '1px solid var(--primary)' : '1px solid var(--border-2)',
background: active ? 'var(--primary)' : 'transparent',
}}>
- {active &&
}
+ {active &&
}
{s.name}
@@ -182,7 +182,7 @@ export default function ServiceSlotPicker({
remove(s.uuid)}
style={{
display: 'grid', placeItems: 'center', width: 18, height: 18, borderRadius: 999, flexShrink: 0,
- border: 'none', cursor: 'pointer', background: 'var(--primary)', color: '#fff',
+ border: 'none', cursor: 'pointer', background: 'var(--primary)', color: 'var(--on-primary)',
fontSize: 13, lineHeight: 1, fontFamily: 'inherit',
}}>×
@@ -211,7 +211,7 @@ export default function ServiceSlotPicker({
style={{
fontSize: 13, padding: '6px 12px', borderRadius: 'var(--r-sm)', cursor: 'pointer', fontFamily: 'inherit',
border: active ? '1px solid var(--primary)' : '1px solid var(--border)',
- background: active ? 'var(--primary)' : 'var(--surface)', color: active ? '#fff' : 'var(--text)',
+ background: active ? 'var(--primary)' : 'var(--surface)', color: active ? 'var(--on-primary)' : 'var(--text)',
}}>
{s.start_time}
diff --git a/assets/admin/components/appointments/TurnsStatInfo.tsx b/assets/admin/components/appointments/TurnsStatInfo.tsx
index d7045a6f..42f90f0c 100644
--- a/assets/admin/components/appointments/TurnsStatInfo.tsx
+++ b/assets/admin/components/appointments/TurnsStatInfo.tsx
@@ -32,10 +32,10 @@ function BadgeIcon({ Icon, color, bg }: { Icon: React.ElementType; color: string
export default function TurnsStatInfo({ stats }: { stats: TurnsStats }) {
const items: StatDef[] = [
- { title: 'کل نوبت های امروز', value: stats.total, Icon: UsersIcon, iconColor: '#494cb3', iconBg: '#e9ebfb' },
- { title: 'نوبت های انجام شده', value: stats.completed, Icon: UserPlusIcon, iconColor: '#0d9f43', iconBg: '#e7f6ed' },
- { title: 'بیماران در انتظار', value: stats.waiting, Icon: ClockIcon, iconColor: '#e0a838', iconBg: '#fff2dc' },
- { title: 'نوبت های لغو شده', value: stats.cancelled, Icon: XCircleIcon, iconColor: '#ee5c6d', iconBg: '#fad6da' },
+ { title: 'کل نوبت های امروز', value: stats.total, Icon: UsersIcon, iconColor: 'var(--primary-600)', iconBg: 'var(--primary-soft)' },
+ { title: 'نوبت های انجام شده', value: stats.completed, Icon: UserPlusIcon, iconColor: 'var(--success)', iconBg: 'var(--success-bg)' },
+ { title: 'بیماران در انتظار', value: stats.waiting, Icon: ClockIcon, iconColor: 'var(--warning)', iconBg: 'var(--warning-bg)' },
+ { title: 'نوبت های لغو شده', value: stats.cancelled, Icon: XCircleIcon, iconColor: 'var(--danger)', iconBg: 'var(--danger-bg)' },
];
return (
diff --git a/assets/admin/components/appointments/TurnsTimeline.tsx b/assets/admin/components/appointments/TurnsTimeline.tsx
index 94345ec4..2a780bf6 100644
--- a/assets/admin/components/appointments/TurnsTimeline.tsx
+++ b/assets/admin/components/appointments/TurnsTimeline.tsx
@@ -77,7 +77,7 @@ function EmptyCard({ slot, onBook }: { slot: TimelineSlot; onBook: (s: TimelineS
cursor: isPast ? 'not-allowed' : 'pointer', opacity: isPast ? 0.7 : 1, minHeight: 76,
}}
>
-
+
{isPast ? 'گذشته' : 'افزودن نوبت سریع'}
@@ -88,8 +88,8 @@ function EmptyCard({ slot, onBook }: { slot: TimelineSlot; onBook: (s: TimelineS
{!isPast && (
نوبت جدید
diff --git a/assets/admin/components/appointments/turnStatus.ts b/assets/admin/components/appointments/turnStatus.ts
index 44a68775..968b91eb 100644
--- a/assets/admin/components/appointments/turnStatus.ts
+++ b/assets/admin/components/appointments/turnStatus.ts
@@ -6,85 +6,33 @@
export interface TurnStatusConfig {
label: string;
bgColor: string;
- darkBgColor: string;
borderColor: string;
- darkBorderColor: string;
dotColor: string;
textColor: string;
- darkTextColor: string;
}
-// وضعیتهای بکاند → رنگ طرح tauri.
+/** یک وضعیت = یک توکن؛ خود توکن با تم عوض میشود، پس جفت light/dark لازم نیست. */
+const of = (fg: string, bg: string): Omit => ({
+ bgColor: `var(--${bg})`, borderColor: `var(--${fg})`,
+ dotColor: `var(--${fg})`, textColor: `var(--${fg})`,
+});
+
+// وضعیتهای بکاند → توکنهای تم (هیوی طرح tauri حفظ شده است).
const STATUS: Record = {
- // pending = «ثبت شده» (tauri: registered)
- pending: {
- label: 'ثبت شده',
- bgColor: '#E3F2FD', darkBgColor: 'rgba(85, 89, 206, 0.18)',
- borderColor: '#2196F3', darkBorderColor: 'rgba(199, 206, 244, 0.42)',
- dotColor: '#2196F3', textColor: '#2196F3', darkTextColor: '#C7CEF4',
- },
- // confirmed = «قطعی شده» (tauri: finalized)
- confirmed: {
- label: 'قطعی شده',
- bgColor: '#E0F2F1', darkBgColor: 'rgba(25, 191, 211, 0.14)',
- borderColor: '#009688', darkBorderColor: 'rgba(25, 191, 211, 0.44)',
- dotColor: '#009688', textColor: '#009688', darkTextColor: '#19BFD3',
- },
- // following_up = «در حال پیگیری» (tauri: in_progress)
- following_up: {
- label: 'در حال پیگیری',
- bgColor: '#FFF3E0', darkBgColor: 'rgba(241, 119, 50, 0.16)',
- borderColor: '#FF9800', darkBorderColor: 'rgba(241, 119, 50, 0.46)',
- dotColor: '#FF9800', textColor: '#d87f00', darkTextColor: '#F17732',
- },
- // salon = «سالن»
- salon: {
- label: 'سالن',
- bgColor: '#F3E5F5', darkBgColor: 'rgba(199, 206, 244, 0.16)',
- borderColor: '#9C27B0', darkBorderColor: 'rgba(199, 206, 244, 0.42)',
- dotColor: '#9C27B0', textColor: '#9C27B0', darkTextColor: '#C7CEF4',
- },
- // completed = «ویزیت شده» (tauri: success)
- completed: {
- label: 'ویزیت شده',
- bgColor: '#E8F5E9', darkBgColor: 'rgba(34, 197, 94, 0.14)',
- borderColor: '#4CAF50', darkBorderColor: 'rgba(34, 197, 94, 0.42)',
- dotColor: '#4CAF50', textColor: '#0d9f43', darkTextColor: '#4ADE80',
- },
- // cancel variants = «لغو شده» (tauri: failed)
- cancelled_by_doctor: {
- label: 'لغو شده',
- bgColor: '#fde7e9', darkBgColor: 'rgba(255, 84, 80, 0.16)',
- borderColor: '#ee5c6d', darkBorderColor: 'rgba(255, 84, 80, 0.44)',
- dotColor: '#ee5c6d', textColor: '#ee5c6d', darkTextColor: '#FF7A76',
- },
- cancelled_by_user: {
- label: 'لغو توسط بیمار',
- bgColor: '#fde7e9', darkBgColor: 'rgba(255, 84, 80, 0.16)',
- borderColor: '#ee5c6d', darkBorderColor: 'rgba(255, 84, 80, 0.44)',
- dotColor: '#ee5c6d', textColor: '#ee5c6d', darkTextColor: '#FF7A76',
- },
- // no_show / expired = خنثی (tauri: pending amber-ish → اینجا خاکستری)
- no_show: {
- label: 'غیبت',
- bgColor: '#f4f4f5', darkBgColor: 'rgba(160, 160, 160, 0.14)',
- borderColor: '#9E9E9E', darkBorderColor: 'rgba(160, 160, 160, 0.4)',
- dotColor: '#9E9E9E', textColor: '#757575', darkTextColor: '#A1A1A1',
- },
- expired: {
- label: 'منقضی',
- bgColor: '#f4f4f5', darkBgColor: 'rgba(160, 160, 160, 0.14)',
- borderColor: '#9E9E9E', darkBorderColor: 'rgba(160, 160, 160, 0.4)',
- dotColor: '#9E9E9E', textColor: '#757575', darkTextColor: '#A1A1A1',
- },
+ pending: { label: 'ثبت شده', ...of('info', 'info-bg') },
+ confirmed: { label: 'قطعی شده', ...of('primary', 'primary-soft') },
+ following_up: { label: 'در حال پیگیری', ...of('accent', 'accent-bg') },
+ salon: { label: 'سالن', ...of('violet', 'violet-bg') },
+ completed: { label: 'ویزیت شده', ...of('success', 'success-bg') },
+ cancelled_by_doctor: { label: 'لغو شده', ...of('danger', 'danger-bg') },
+ cancelled_by_user: { label: 'لغو توسط بیمار', ...of('danger', 'danger-bg') },
+ no_show: { label: 'غیبت', ...of('text-3', 'surface-2') },
+ expired: { label: 'منقضی', ...of('text-3', 'surface-2') },
};
// اسلات خالی (نوبت جدید) — tauri: empty.
export const EMPTY_SLOT_CONFIG: TurnStatusConfig = {
- label: 'نوبت جدید',
- bgColor: '#E3F2FD', darkBgColor: 'rgba(85, 89, 206, 0.16)',
- borderColor: '#2196F3', darkBorderColor: 'rgba(199, 206, 244, 0.38)',
- dotColor: '#2196F3', textColor: '#5559ce', darkTextColor: '#C7CEF4',
+ label: 'نوبت جدید', ...of('primary', 'primary-soft'),
};
export function turnStatusConfig(status: string): TurnStatusConfig {
diff --git a/assets/admin/components/dashboard/NewAppointmentsTable.tsx b/assets/admin/components/dashboard/NewAppointmentsTable.tsx
index 57cb6224..b5b392d4 100644
--- a/assets/admin/components/dashboard/NewAppointmentsTable.tsx
+++ b/assets/admin/components/dashboard/NewAppointmentsTable.tsx
@@ -56,20 +56,20 @@ export function NewAppointmentsTable({ rows, loading, queryKey, emptyText }: Pro
}
if (!rows.length) {
return (
-
+
{emptyText ?? 'نوبتی برای امروز ثبت نشده'}
);
}
return (
-
+
-
+
{HEAD.map((h, i) => (
{h}
@@ -80,7 +80,7 @@ export function NewAppointmentsTable({ rows, loading, queryKey, emptyText }: Pro
{rows.map((r, idx) => (
| {new Intl.NumberFormat('fa-IR').format(idx + 1)} |
{r.patient_name || '—'} |
@@ -97,7 +97,7 @@ export function NewAppointmentsTable({ rows, loading, queryKey, emptyText }: Pro
مشاهده
@@ -112,7 +112,7 @@ export function NewAppointmentsTable({ rows, loading, queryKey, emptyText }: Pro
/** نمایش فقطخواندنی وضعیت — وقتی version یا queryKey در دسترس نیست. */
function StatusPill({ status }: { status: string }) {
- const meta = STATUS_META[status] ?? { label: status, color: '#9ca3af' };
+ const meta = STATUS_META[status] ?? { label: status, color: 'var(--text-3)' };
return (
{children}
diff --git a/assets/admin/components/dashboard/TauriCharts.test.tsx b/assets/admin/components/dashboard/TauriCharts.test.tsx
index 59a484bc..3d214553 100644
--- a/assets/admin/components/dashboard/TauriCharts.test.tsx
+++ b/assets/admin/components/dashboard/TauriCharts.test.tsx
@@ -31,7 +31,7 @@ describe('TauriBarChart', () => {
it('renders one bar per day of a full Jalali month', () => {
const month: ChartPoint[] = Array.from({ length: 31 }, (_, i) => ({ label: String(i + 1), value: i % 5 }));
const { container } = render( );
- expect(container.querySelectorAll('.bg-\\[\\#5559CE\\]')).toHaveLength(31);
+ expect(container.querySelectorAll('[class*="bg-[var(--primary)]"]')).toHaveLength(31);
});
/**
@@ -44,7 +44,7 @@ describe('TauriBarChart', () => {
const month: ChartPoint[] = Array.from({ length: 31 }, (_, i) => ({ label: String(i + 1), value: i % 5 }));
const { container } = render( );
- const bar = container.querySelector('.bg-\\[\\#5559CE\\]') as HTMLElement;
+ const bar = container.querySelector('[class*="bg-[var(--primary)]"]') as HTMLElement;
// کسری از ستون، نه تمام آن — وگرنه میلهها بدون فاصله به هم میچسبند
expect(bar.className).toMatch(/\bw-\[\d+%\]/);
expect(bar.className.split(/\s+/)).not.toContain('w-full');
diff --git a/assets/admin/components/dashboard/TauriCharts.tsx b/assets/admin/components/dashboard/TauriCharts.tsx
index 1073e5b1..23fb1f53 100644
--- a/assets/admin/components/dashboard/TauriCharts.tsx
+++ b/assets/admin/components/dashboard/TauriCharts.tsx
@@ -91,7 +91,7 @@ function ChartFrame({
{/* y-axis ticks, aligned to gridlines */}
{ticks.map((t, i) => (
@@ -106,7 +106,7 @@ function ChartFrame({
{ticks.map((_, i) => (
))}
@@ -116,7 +116,7 @@ function ChartFrame({
{/* x-axis labels */}
{labels.map((l, i) => (
-
+
{l}
))}
@@ -145,7 +145,7 @@ export function TauriBarChart({ data }: { data: ChartPoint[] }) {
0 ? 4 : 0,
@@ -278,7 +278,7 @@ export function TauriLineChart({ data, actualCount }: { data: ChartPoint[]; actu
top: `${(pts[i][1] / H) * 100}%`,
transform: 'translate(-50%, calc(-100% - 12px))',
background: isForecast ? 'var(--accent)' : 'var(--text)',
- color: '#fff',
+ color: 'var(--on-primary)',
boxShadow: 'var(--shadow)',
}}
>
@@ -302,7 +302,7 @@ export function TauriLineChart({ data, actualCount }: { data: ChartPoint[]; actu
function EmptyChart() {
return (
-
+
دادهای برای نمایش نیست
);
diff --git a/assets/admin/components/dashboard/TauriDashboardView.tsx b/assets/admin/components/dashboard/TauriDashboardView.tsx
index ae042fc5..02625ff1 100644
--- a/assets/admin/components/dashboard/TauriDashboardView.tsx
+++ b/assets/admin/components/dashboard/TauriDashboardView.tsx
@@ -31,7 +31,7 @@ function SmSelector({ options, value, onChange }: { options: SelectorOption[]; v
}
function ChartTitle({ text }: { text: string }) {
- return
{text}
;
+ return
{text}
;
}
function ChartCard({
@@ -48,7 +48,7 @@ function ChartCard({
children: React.ReactNode;
}) {
return (
-
+
@@ -141,12 +141,12 @@ export function TauriDashboardView({
{/* New appointments list */}
-
+
-
لیست نوبتهای جدید
+
لیست نوبتهای جدید
نوبتها
diff --git a/assets/admin/components/dashboard/TauriStatCards.tsx b/assets/admin/components/dashboard/TauriStatCards.tsx
index 0428a4e3..f8d6bb5a 100644
--- a/assets/admin/components/dashboard/TauriStatCards.tsx
+++ b/assets/admin/components/dashboard/TauriStatCards.tsx
@@ -19,7 +19,7 @@ export interface StatCardModel {
function TauriStatCard({ data }: { data: StatCardModel }) {
return (
-
+
{data.value}
-
+
{data.label}
@@ -65,28 +65,28 @@ export function TauriStatCards({
const cards: StatCardModel[] = [
{
cardColor: 'bg-[rgba(241,119,50,0.10)]',
- iconColor: 'bg-[#F17732]',
+ iconColor: 'bg-[var(--accent)]',
icon:
,
value: `${formatNumber(stats.totalPatients)}+`,
label: 'تعداد کل مراجعین',
},
{
cardColor: 'bg-[rgba(0,157,121,0.10)]',
- iconColor: 'bg-[#009D79]',
+ iconColor: 'bg-[var(--success)]',
icon:
,
value: formatRial(stats.totalPaymentsRials),
label: 'کل پرداختیها',
},
{
cardColor: 'bg-[rgba(85,89,206,0.08)]',
- iconColor: 'bg-[#5559CE]',
+ iconColor: 'bg-[var(--primary)]',
icon:
,
value: formatRial(stats.todayPaymentsRials),
label: 'پرداختیهای امروز',
},
{
cardColor: 'bg-[rgba(255,192,81,0.11)]',
- iconColor: 'bg-[#FFC051]',
+ iconColor: 'bg-[var(--warning)]',
icon:
,
value: `${formatNumber(stats.todayAppointments)}+`,
label: 'تعداد نوبتهای امروز',
diff --git a/assets/admin/components/dashboard/dashboardIcons.tsx b/assets/admin/components/dashboard/dashboardIcons.tsx
index da95f4fa..35ed7229 100644
--- a/assets/admin/components/dashboard/dashboardIcons.tsx
+++ b/assets/admin/components/dashboard/dashboardIcons.tsx
@@ -68,10 +68,10 @@ export function CalendarIcon() {
}
/** فلش رو به پایین — status chip chevron (source: ArrowDownBlueP) */
-export function ChevronDownIcon({ color = '#5559CE' }: { color?: string }) {
+export function ChevronDownIcon({ color = 'currentColor' }: { color?: string }) {
return (
-
-
+
+
);
}
diff --git a/assets/admin/components/icons/FilesServiceIcons.tsx b/assets/admin/components/icons/FilesServiceIcons.tsx
index 864f431e..3020bce0 100644
--- a/assets/admin/components/icons/FilesServiceIcons.tsx
+++ b/assets/admin/components/icons/FilesServiceIcons.tsx
@@ -8,22 +8,22 @@ type IconProps = { color?: string; style?: CSSProperties };
/* ── Service card ─────────────────────────────────────────────────────────── */
-export function FilesServiceSuccess({ style }: { style?: CSSProperties }) {
+export function FilesServiceSuccess({ style }: { style?: CSSProperties }){
return (
-
-
-
+
+
+
);
}
-export function FilesServiceMore({ style }: { style?: CSSProperties }) {
+export function FilesServiceMore({ style }: { style?: CSSProperties }){
return (
-
-
-
+
+
+
);
}
@@ -33,10 +33,10 @@ export function FilesServiceMore({ style }: { style?: CSSProperties }) {
export function FilesServicePhone() {
return (
-
-
-
-
+
+
+
+
);
}
@@ -44,16 +44,16 @@ export function FilesServicePhone() {
export function FilesServiceCalendar() {
return (
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
}
@@ -61,9 +61,9 @@ export function FilesServiceCalendar() {
export function FilesServiceNotification() {
return (
-
-
-
+
+
+
);
}
@@ -71,17 +71,17 @@ export function FilesServiceNotification() {
export function FilesServiceMessage() {
return (
-
-
-
+
+
+
);
}
-export function ArrowLeftPH({ style }: { style?: CSSProperties }) {
+export function ArrowLeftPH({ style }: { style?: CSSProperties }){
return (
-
+
);
}
@@ -89,163 +89,163 @@ export function ArrowLeftPH({ style }: { style?: CSSProperties }) {
export function ArrowLeftD() {
return (
-
-
+
+
);
}
-/* ── Tab icons (color = active #5559ce / inactive #616161) ─────────────────── */
+/* ── Tab icons (رنگ از توکن والد؛ فعال var(--primary) / غیرفعال var(--text-2)) ── */
-export function TabServices({ color = '#616161', style }: IconProps) {
+export function TabServices({ color = 'currentColor', style }: IconProps) {
return (
-
-
-
-
-
-
+
+
+
+
+
+
);
}
-export function TabInfo({ color = '#616161', style }: IconProps) {
+export function TabInfo({ color = 'currentColor', style }: IconProps) {
return (
-
-
-
-
-
-
+
+
+
+
+
+
);
}
-export function TabCalendar({ color = '#616161', style }: IconProps) {
+export function TabCalendar({ color = 'currentColor', style }: IconProps) {
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
}
-export function TabCard({ color = '#616161', style }: IconProps) {
+export function TabCard({ color = 'currentColor', style }: IconProps) {
return (
-
-
-
-
-
-
+
+
+
+
+
+
);
}
-export function TabWallet({ color = '#616161', style }: IconProps) {
+export function TabWallet({ color = 'currentColor', style }: IconProps) {
return (
-
-
-
-
-
+
+
+
+
+
);
}
-export function TabSMS({ color = '#616161', style }: IconProps) {
+export function TabSMS({ color = 'currentColor', style }: IconProps) {
return (
-
-
-
+
+
+
);
}
-export function TabCall({ color = '#616161', style }: IconProps) {
+export function TabCall({ color = 'currentColor', style }: IconProps) {
return (
-
-
+
+
);
}
-export function TabAttach({ color = '#616161', style }: IconProps) {
+export function TabAttach({ color = 'currentColor', style }: IconProps) {
return (
-
-
-
+
+
+
);
}
-export function TabBody({ color = '#616161', style }: IconProps) {
+export function TabBody({ color = 'currentColor', style }: IconProps) {
return (
-
-
-
+
+
+
);
}
/** Payment check icon (tauri FilesServicePaymentsCheckIcon) — rounded-square tick. */
-export function FilesServicePaymentsCheck({ color = '#2F2F2F', size = 20, style }: IconProps & { size?: number }) {
+export function FilesServicePaymentsCheck({ color = 'currentColor', size = 20, style }: IconProps & { size?: number }) {
return (
-
-
-
+
+
+
);
}
/* ── Turn card info-row icons (tauri CalendarD / ClockP / UserD / status) ───── */
-export function CalendarD({ color = '#616161', size = 20, style }: IconProps & { size?: number }) {
+export function CalendarD({ color = 'currentColor', size = 20, style }: IconProps & { size?: number }) {
return (
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
);
}
-export function ClockP({ color = '#525252', size = 20, style }: IconProps & { size?: number }) {
+export function ClockP({ color = 'currentColor', size = 20, style }: IconProps & { size?: number }) {
return (
-
-
-
+
+
+
);
}
-export function UserD({ color = '#616161', size = 20, style }: IconProps & { size?: number }) {
+export function UserD({ color = 'currentColor', size = 20, style }: IconProps & { size?: number }) {
return (
-
-
-
+
+
+
);
}
-export function StatusGlobe({ color = '#616161', size = 20, style }: IconProps & { size?: number }) {
+export function StatusGlobe({ color = 'currentColor', size = 20, style }: IconProps & { size?: number }) {
return (
-
-
-
-
+
+
+
+
);
}
@@ -255,58 +255,58 @@ export function StatusGlobe({ color = '#616161', size = 20, style }: IconProps &
type StepIconProps = { size?: number; active?: boolean };
/** Step «ایجاد سرویس/ویرایش» icon — tauri FilesServiceAddServiceStep, verbatim. */
-export function FilesServiceAddServiceStep({ size = 52, active = false }: StepIconProps) {
- const circleStrokeColor = active ? '#5559CE' : '#E1E1E1';
- const bgColor = active ? '#5559CE' : '#E1E1E1';
- const iconColor = active ? '#FAFAFA' : '#9B9B9B';
+export function FilesServiceAddServiceStep({ size = 52, active = false }: StepIconProps){
+ const circleStrokeColor = active ? 'var(--primary)' : 'var(--border-2)';
+ const bgColor = active ? 'var(--primary)' : 'var(--border-2)';
+ const iconColor = active ? 'var(--on-primary)' : 'var(--text-3)';
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
}
/** Step «پرداخت» icon — tauri FilesAddServicePaymentStep, verbatim. */
-export function FilesAddServicePaymentStep({ size = 52, active = false }: StepIconProps) {
- const circleStrokeColor = active ? '#5559CE' : '#E1E1E1';
- const bgColor = active ? '#5559CE' : '#E1E1E1';
- const iconColor = active ? '#FAFAFA' : '#9B9B9B';
+export function FilesAddServicePaymentStep({ size = 52, active = false }: StepIconProps){
+ const circleStrokeColor = active ? 'var(--primary)' : 'var(--border-2)';
+ const bgColor = active ? 'var(--primary)' : 'var(--border-2)';
+ const iconColor = active ? 'var(--on-primary)' : 'var(--text-3)';
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
}
/** Step «جزییات» icon — tauri FilesServiceDetailsStep, verbatim. */
-export function FilesServiceDetailsStep({ size = 52, active = false }: StepIconProps) {
- const circleStrokeColor = active ? '#5559CE' : '#E1E1E1';
- const bgColor = active ? '#5559CE' : '#E1E1E1';
- const iconColor = active ? '#FAFAFA' : '#9B9B9B';
+export function FilesServiceDetailsStep({ size = 52, active = false }: StepIconProps){
+ const circleStrokeColor = active ? 'var(--primary)' : 'var(--border-2)';
+ const bgColor = active ? 'var(--primary)' : 'var(--border-2)';
+ const iconColor = active ? 'var(--on-primary)' : 'var(--text-3)';
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
}
@@ -314,38 +314,38 @@ export function FilesServiceDetailsStep({ size = 52, active = false }: StepIconP
/* ── Payment step widgets (tauri Step2Payment) ───────────────────────────── */
/** Orange «هزینه سرویس» badge — tauri Step2PaymentCard, verbatim. */
-export function Step2PaymentCard({ size = 40 }: { size?: number }) {
+export function Step2PaymentCard({ size = 40 }: { size?: number }){
return (
-
-
-
-
+
+
+
+
);
}
/** «موجودی کیف پول» icon — tauri FilesServiceBalanceWallet, verbatim. */
-export function FilesServiceBalanceWallet({ size = 20 }: { size?: number }) {
+export function FilesServiceBalanceWallet({ size = 20 }: { size?: number }){
return (
-
-
-
-
+
+
+
+
);
}
/** Red trash icon (حذف تخفیف) — tauri TrashRed, verbatim. */
-export function TrashRed({ size = 20, color = '#FF5450' }: { size?: number; color?: string }) {
+export function TrashRed({ size = 20, color = 'currentColor' }: { size?: number; color?: string }) {
return (
-
-
-
-
-
-
+
+
+
+
+
+
);
}
@@ -354,33 +354,33 @@ export function TrashRed({ size = 20, color = '#FF5450' }: { size?: number; colo
export function CloseModalD() {
return (
-
+
);
}
/** «پذیرش کننده» — tauri UserTick, verbatim. */
-export function UserTick({ color = '#0D9F43', size = 24 }: { color?: string; size?: number }) {
+export function UserTick({ color = 'currentColor', size = 24 }: { color?: string; size?: number }) {
return (
-
-
-
-
-
+
+
+
+
+
);
}
/** آیکون «قیمت» کنار سرویس/کالا — tauri FilesServiceAddCard, verbatim. */
-export function FilesServiceAddCard({ color = '#616161' }: { color?: string }) {
+export function FilesServiceAddCard({ color = 'currentColor' }: { color?: string }) {
return (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
);
}
diff --git a/assets/admin/components/icons/FilesToolbarIcons.tsx b/assets/admin/components/icons/FilesToolbarIcons.tsx
index 95a33bd5..fbb5e1d8 100644
--- a/assets/admin/components/icons/FilesToolbarIcons.tsx
+++ b/assets/admin/components/icons/FilesToolbarIcons.tsx
@@ -3,56 +3,56 @@
* پروندهها header matches the tauri /files page pixel-for-pixel.
*/
-export function SearchHeaderP({ color = '#7E7E7E' }: { color?: string }) {
+export function SearchHeaderP({ color = 'currentColor' }: { color?: string }) {
return (
-
-
-
+
+
+
);
}
-export function TurnsFilter({ color = '#616161' }: { color?: string }) {
+export function TurnsFilter({ color = 'currentColor' }: { color?: string }) {
return (
-
-
-
-
-
+
+
+
+
+
);
}
/** Table-view toggle icon (tauri `PatientsGridView`). */
-export function PatientsGridView({ width = 24, height = 24, color = '#5559CE' }: { width?: number; height?: number; color?: string }) {
+export function PatientsGridView({ width = 24, height = 24, color = 'currentColor' }: { width?: number; height?: number; color?: string }) {
return (
-
-
-
-
-
-
+
+
+
+
+
+
);
}
/** Card-view toggle icon (tauri `PatientsCategoryView`). */
-export function PatientsCategoryView({ width = 24, height = 24, color = '#616161' }: { width?: number; height?: number; color?: string }) {
+export function PatientsCategoryView({ width = 24, height = 24, color = 'currentColor' }: { width?: number; height?: number; color?: string }) {
return (
-
-
-
-
-
+
+
+
+
+
);
}
-export function AddTurn({ color = '#E1E1E1' }: { color?: string }) {
+export function AddTurn({ color = 'currentColor' }: { color?: string }) {
return (
-
-
-
+
+
+
);
}
diff --git a/assets/admin/components/inventory/AddPackageModal.tsx b/assets/admin/components/inventory/AddPackageModal.tsx
index ca30a437..c364a2ac 100644
--- a/assets/admin/components/inventory/AddPackageModal.tsx
+++ b/assets/admin/components/inventory/AddPackageModal.tsx
@@ -101,7 +101,7 @@ export default function AddPackageModal({ open, editing, items, saving, onClose,
@@ -114,9 +114,9 @@ export default function AddPackageModal({ open, editing, items, saving, onClose,
{l.name}
- changeAmount(idx, 1)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#f0753b', fontSize: 18, fontWeight: 600 }}>+
- {l.amount}
- changeAmount(idx, -1)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#f0753b', fontSize: 18, fontWeight: 600 }}>-
+ changeAmount(idx, 1)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--accent)', fontSize: 18, fontWeight: 600 }}>+
+ {l.amount}
+ changeAmount(idx, -1)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--accent)', fontSize: 18, fontWeight: 600 }}>-
{formatRial(l.price)}
removeLine(idx)} aria-label="حذف" style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--danger)' }}>
diff --git a/assets/admin/components/inventory/InventoryActionsMenu.tsx b/assets/admin/components/inventory/InventoryActionsMenu.tsx
index ac7fd85e..201fd12d 100644
--- a/assets/admin/components/inventory/InventoryActionsMenu.tsx
+++ b/assets/admin/components/inventory/InventoryActionsMenu.tsx
@@ -55,7 +55,7 @@ export default function InventoryActionsMenu({ item, onEdit, onDelete }: Props)
style={{
display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer',
borderRadius: 6, padding: '4px 8px', background: 'none',
- color: '#5559ce', fontSize: 12, fontWeight: 500,
+ color: 'var(--primary)', fontSize: 12, fontWeight: 500,
}}
>
diff --git a/assets/admin/components/inventory/InventoryItemsTable.tsx b/assets/admin/components/inventory/InventoryItemsTable.tsx
index 41a6c8c8..611b5c92 100644
--- a/assets/admin/components/inventory/InventoryItemsTable.tsx
+++ b/assets/admin/components/inventory/InventoryItemsTable.tsx
@@ -24,7 +24,7 @@ export default function InventoryItemsTable({ items, onEdit, onDelete }: Props)
{/* Desktop table */}
diff --git a/assets/admin/components/inventory/PackagesView.tsx b/assets/admin/components/inventory/PackagesView.tsx
index 25b9c017..66a04b8c 100644
--- a/assets/admin/components/inventory/PackagesView.tsx
+++ b/assets/admin/components/inventory/PackagesView.tsx
@@ -26,22 +26,22 @@ function PackageCard({ pkg, onEdit, onDelete }: { pkg: InventoryPackage } & Pick
const canUpdate = can('inventory', 'update');
const canDelete = can('inventory', 'delete');
const [expanded, setExpanded] = useState(false);
- const divider =
;
+ const divider =
;
return (
{/* Header */}
-
+
{pkg.title}
{pkg.available ? 'موجودی کافی' : 'موجودی ناکافی'}
diff --git a/assets/admin/components/layout/PurchaseSubscriptionSidebar.tsx b/assets/admin/components/layout/PurchaseSubscriptionSidebar.tsx
index a489eb7a..96bfcc36 100644
--- a/assets/admin/components/layout/PurchaseSubscriptionSidebar.tsx
+++ b/assets/admin/components/layout/PurchaseSubscriptionSidebar.tsx
@@ -115,7 +115,7 @@ export default function PurchaseSubscriptionSidebar({ active }: { active: string
padding: '0 14px', fontSize: 16, fontWeight: isActive ? 700 : 500,
lineHeight: 1, textAlign: 'right',
background: isActive ? 'var(--accent)' : 'transparent',
- color: isActive ? '#FFFFFF' : 'var(--text-2)',
+ color: isActive ? 'var(--on-primary)' : 'var(--text-2)',
cursor: item.to ? 'pointer' : 'not-allowed',
transition: 'background .14s',
};
diff --git a/assets/admin/components/layout/Topbar.tsx b/assets/admin/components/layout/Topbar.tsx
index f95e522d..bbabea39 100644
--- a/assets/admin/components/layout/Topbar.tsx
+++ b/assets/admin/components/layout/Topbar.tsx
@@ -122,7 +122,7 @@ export default function Topbar({ onMobileMenuOpen }: { onMobileMenuOpen?: () =>
width: 40, height: 40, borderRadius: 10,
background: hue.fixed?.primary ?? `oklch(0.55 ${hue.c} ${hue.h})`,
border: brandHue.h === hue.h ? '3px solid var(--text)' : '3px solid transparent',
- display: 'grid', placeItems: 'center', color: '#fff', cursor: 'pointer',
+ display: 'grid', placeItems: 'center', color: 'var(--on-primary)', cursor: 'pointer',
}}
>
{brandHue.h === hue.h && }
diff --git a/assets/admin/components/schedule/ScheduleSection.tsx b/assets/admin/components/schedule/ScheduleSection.tsx
index 1676ee84..a5692c65 100644
--- a/assets/admin/components/schedule/ScheduleSection.tsx
+++ b/assets/admin/components/schedule/ScheduleSection.tsx
@@ -169,30 +169,30 @@ function PersianDateInput({ value, onChange, minDate, placeholder }: {
-
+
{value ? formatPersianDate(value) : (placeholder ?? 'انتخاب تاریخ')}
-
+
{open && createPortal(
-
+ className="bg-[var(--surface)] border border-[var(--border)] rounded-2xl shadow-2xl overflow-hidden">
+
+ className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] text-[var(--text-2)] transition-colors">
-
+
{viewJm > 0 ? JALALI_MONTHS[viewJm - 1] : ''} {toPersianNums(viewJy)}
+ className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] text-[var(--text-2)] transition-colors">
{JALALI_DAYS_SHORT.map(d => (
-
{d}
+
{d}
))}
@@ -205,10 +205,10 @@ function PersianDateInput({ value, onChange, minDate, placeholder }: {
return (
selectDay(jd)}
className={`w-full aspect-square flex items-center justify-center rounded-lg text-sm transition-colors
- ${dis ? 'text-slate-300 dark:text-gray-600 cursor-not-allowed'
- : sel ? 'bg-primary-500 text-white font-semibold shadow-sm'
+ ${dis ? 'text-[var(--text-3)] dark:text-[var(--text-2)] cursor-not-allowed'
+ : sel ? 'bg-primary-500 text-[var(--on-primary)] font-semibold shadow-sm'
: tod ? 'ring-1 ring-primary-400 text-primary-700 dark:text-primary-300 font-medium'
- : 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-gray-800'}`}>
+ : 'text-[var(--text)] dark:text-[var(--text-3)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)]'}`}>
{toPersianNums(jd)}
);
@@ -331,10 +331,10 @@ function SlotEditor({ slots, onChange }: {
{slots.map((slot, i) => (
-
از
+
از
update(i, 'start', e.target.value)}
className="cp-input h-8 w-28 text-sm font-mono text-center px-2" />
-
تا
+
تا
update(i, 'end', e.target.value)}
className="cp-input h-8 w-28 text-sm font-mono text-center px-2" />
@@ -346,7 +346,7 @@ function SlotEditor({ slots, onChange }: {
/>
remove(i)}
- className="w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors shrink-0">
+ className="w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--danger)] hover:bg-[var(--danger-bg)] dark:hover:bg-[var(--danger)]/10 transition-colors shrink-0">
@@ -441,7 +441,7 @@ function SessionEditor({ session, onChange, onRemove, addresses, serviceMode = f
}}>
@@ -511,12 +511,12 @@ function NoLocationsNotice({ clinicUuid }: { clinicUuid?: string | null }) {
: ['ابتدا آدرس مطب را ثبت کنید', 'برنامه کاری نیاز به حداقل یک مکان نوبت دارد'];
return (
-
-
+
+
-
{title}
-
{hint}
+
{title}
+
{hint}
);
@@ -627,8 +627,8 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
return (
{totalSlots > 0 && (
-
-
+
+
مجموع {totalSlots} نوبت در هفته
@@ -638,11 +638,11 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
const activeSessions = (scheduleMap[day.key]?.sessions ?? []).filter(s => s.active);
const daySlots = activeSessions.reduce((sum, s) => sum + calcSlotCount(s), 0);
return (
-
+
{day.label}
{activeSessions.length > 0 ? activeSessions.map((s, i) => (
-
+
{s.start_time} – {s.end_time}
)) : (
@@ -665,8 +665,8 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
{/* ─ خلاصه کل هفته */}
{totalSlots > 0 && (
-
-
+
+
مجموع {totalSlots} نوبت در هفته
@@ -674,9 +674,9 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
)}
{/* ─ روش نوبتدهی */}
-
-
-
روش نوبتدهی
+
+
+ روش نوبتدهی
@@ -694,48 +694,48 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
className={`text-right p-3 rounded-lg border transition-colors ${
selected
? 'border-[var(--primary)] bg-[var(--primary)]/5'
- : 'border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-900'
+ : 'border-[var(--border)] dark:border-[var(--border)] bg-[var(--surface)] dark:bg-[var(--bg)]'
} ${modeLocked ? 'opacity-70 cursor-not-allowed' : 'hover:border-[var(--primary)]'}`}
>
-
- {lbl}
+
+ {lbl}
-
{desc}
+
{desc}
);
})}
{modeLocked ? (
-
+
نوع نوبتدهی ثبت شده و دیگر قابل تغییر نیست.
) : (
-
+
⚠️ توجه: نوع نوبتدهی پس از اولین ثبت بههیچعنوان قابل تغییر نیست . پیش از ذخیره با دقت انتخاب کنید.
)}
{meta.booking_mode === 'service' ? (
<>
- فاصله بین نوبتها
+ فاصله بین نوبتها
setMeta(m => ({ ...m, buffer_minutes: Math.max(0, Number(digitsOnly(e.target.value)) || 0) }))}
- className="w-16 text-center text-sm rounded-lg border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1.5 focus:outline-none focus:ring-0"
+ className="w-16 text-center text-sm rounded-lg border border-[var(--border)] bg-[var(--surface)] px-2 py-1.5 focus:outline-none focus:ring-0"
/>
- دقیقه
+ دقیقه
-
+
مدت هر نوبت از «مدت سرویس» انتخابشده تعیین میشود. لازم است حداقل یک سرویس با «نمایش در نوبتدهی» در بخش {clinicUuid ? 'سرویسهای کلینیک' : 'سرویسها'} تعریف کنید، وگرنه ذخیره نمیشود.
>
) : (
-
+
مدت هر نوبت از «زمان هر نوبت» در شیفتهای زیر تعیین میشود.
)}
@@ -743,12 +743,12 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
{/* ─ نوبتدهی آنلاین */}
-
+
{/* header + toggle */}
-
+
-
- نوبتدهی آنلاین
+
+ نوبتدهی آنلاین
setMeta(m => ({ ...m, online_booking_enabled: e.target.checked }))}
/>
-
+
{/* booking window control */}
-
رزرو آنلاین تا
+
رزرو آنلاین تا
-
آینده
+
آینده
-
+
بیمار فقط تا این بازه میتواند آنلاین نوبت بگیرد؛ روزهای بعد از آن روی تقویم غیرفعالاند.
@@ -864,7 +864,7 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
{(hasAnyOverlap || missingLocation) && (
-
+
{hasAnyOverlap ? 'تداخل زمانی در برنامه وجود دارد' : 'مکان مطب برای همه بازهها الزامی است'}
@@ -956,17 +956,17 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, clinicUuid, on
>
{/* ─ نوع override */}
-
نوع این روز
+
نوع این روز
setOverrideType('closed')}
- className={`flex items-center gap-2 p-3 rounded-xl border-2 text-sm transition-colors text-right ${overrideType === 'closed' ? 'border-red-400 bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-300' : 'border-slate-200 dark:border-gray-700 text-slate-600 dark:text-slate-400 hover:border-slate-300'}`}>
+ className={`flex items-center gap-2 p-3 rounded-xl border-2 text-sm transition-colors text-right ${overrideType === 'closed' ? 'border-red-400 bg-[var(--danger-bg)] dark:bg-[var(--danger)]/10 text-[var(--danger)] dark:text-red-300' : 'border-[var(--border)] dark:border-[var(--border)] text-[var(--text-2)] dark:text-[var(--text-3)] hover:border-[var(--border-2)]'}`}>
تعطیل است
@@ -975,7 +975,7 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, clinicUuid, on
setOverrideType('custom')}
- className={`flex items-center gap-2 p-3 rounded-xl border-2 text-sm transition-colors text-right ${overrideType === 'custom' ? 'border-amber-400 bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-300' : 'border-slate-200 dark:border-gray-700 text-slate-600 dark:text-slate-400 hover:border-slate-300'}`}>
+ className={`flex items-center gap-2 p-3 rounded-xl border-2 text-sm transition-colors text-right ${overrideType === 'custom' ? 'border-amber-400 bg-[var(--warning-bg)] dark:bg-amber-500/10 text-amber-700 dark:text-amber-300' : 'border-[var(--border)] dark:border-[var(--border)] text-[var(--text-2)] dark:text-[var(--text-3)] hover:border-[var(--border-2)]'}`}>
ساعات خاص
@@ -986,7 +986,7 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, clinicUuid, on
-
دلیل (اختیاری)
+
دلیل (اختیاری)
setReason(e.target.value)}
placeholder={overrideType === 'closed' ? 'مثال: سفر، مریضی، کنگره...' : 'مثال: شیفت اضطراری، ویزیت خاص...'}
className="input" />
@@ -1020,9 +1020,9 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, clinicUuid, on
)}
{overrideType === 'closed' && (
-
-
-
+
+
+
در این روز هیچ نوبتی داده نخواهد شد، حتی اگر در برنامه هفتگی ساعات کاری داشته باشید.
@@ -1072,37 +1072,37 @@ function DateOverridesTab({ doctorUuid, clinicUuid, addresses, readOnly = false
)}
{overrides.length === 0 ? (
-
+
هیچ تاریخ خاصی تنظیم نشده
) : (
{overrides.map(ov => (
-
+
- {formatPersianDate(tsToDate(ov.date))}
+ {formatPersianDate(tsToDate(ov.date))}
{ov.active ? (
-
+
{ov.custom_slots.length > 0 ? `${ov.custom_slots.length} بازه سفارشی` : 'ساعات خاص'}
) : (
-
+
تعطیل
)}
- {ov.reason &&
{ov.reason}
}
+ {ov.reason &&
{ov.reason}
}
{!readOnly && (
{ setEditing(ov); setModalOpen(true); }}
- className="w-8 h-8 flex items-center justify-center rounded-lg text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-500/10 transition-colors">
+ className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--info)] hover:bg-[var(--info-bg)] dark:hover:bg-blue-500/10 transition-colors">
setDeletingUuid(ov.uuid)}
- className="w-8 h-8 flex items-center justify-center rounded-lg text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors">
+ className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--danger)] hover:bg-[var(--danger-bg)] dark:hover:bg-[var(--danger)]/10 transition-colors">
@@ -1173,19 +1173,19 @@ function HolidayModal({ open, onClose, existing, doctorUuid, clinicUuid, onSaved
-
از تاریخ
+
از تاریخ
{ setStartDate(v); if (endDate && v > endDate) setEndDate(v); }} />
{endDate && startDate && endDate < startDate && (
-
تاریخ پایان نمیتواند قبل از تاریخ شروع باشد
+
تاریخ پایان نمیتواند قبل از تاریخ شروع باشد
)}
- دلیل (اختیاری)
+ دلیل (اختیاری)
setReason(e.target.value)}
placeholder="مثال: سفر، کنگره پزشکی..." className="input" />
@@ -1238,7 +1238,7 @@ function HolidaysTab({ doctorUuid, clinicUuid, readOnly = false }: { doctorUuid:
)}
{holidays.length === 0 ? (
-
+
@@ -1250,39 +1250,39 @@ function HolidaysTab({ doctorUuid, clinicUuid, readOnly = false }: { doctorUuid:
const isCurrent = h.start_date <= now && h.end_date >= now;
const sameDay = tsToDate(h.start_date) === tsToDate(h.end_date);
return (
-
+
-
+
{sameDay ? formatPersianDate(tsToDate(h.start_date)) : `${formatPersianDate(tsToDate(h.start_date))} تا ${formatPersianDate(tsToDate(h.end_date))}`}
{isCurrent && h.active && (
- در جریان
+ در جریان
)}
{!isCurrent && !isPast && h.active && (
آینده
)}
{isPast && (
- گذشته
+ گذشته
)}
{!h.active && (
- غیرفعال
+ غیرفعال
)}
- {h.reason &&
{h.reason}
}
+ {h.reason &&
{h.reason}
}
{!readOnly && (
toggleMut.mutate(h)} disabled={toggleMut.isPending}
- className={`text-xs px-2.5 h-7 rounded-lg border transition-colors disabled:opacity-50 ${h.active ? 'border-slate-200 dark:border-gray-600 text-slate-500 dark:text-slate-400 hover:border-slate-300 hover:text-slate-700 dark:hover:text-slate-200' : 'border-emerald-200 dark:border-emerald-500/40 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-500/10'}`}>
+ className={`text-xs px-2.5 h-7 rounded-lg border transition-colors disabled:opacity-50 ${h.active ? 'border-[var(--border)] dark:border-gray-600 text-[var(--text-2)] dark:text-[var(--text-3)] hover:border-[var(--border-2)] hover:text-[var(--text)] dark:hover:text-[var(--text)]' : 'border-emerald-200 dark:border-emerald-500/40 text-[var(--success)] dark:text-emerald-400 hover:bg-[var(--success-bg)] dark:hover:bg-emerald-500/10'}`}>
{h.active ? 'غیرفعال' : 'فعال'}
{ setEditing(h); setModalOpen(true); }}
- className="w-8 h-8 flex items-center justify-center rounded-lg text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-500/10 transition-colors">
+ className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--info)] hover:bg-[var(--info-bg)] dark:hover:bg-blue-500/10 transition-colors">
setDeletingUuid(h.uuid)}
- className="w-8 h-8 flex items-center justify-center rounded-lg text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors">
+ className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--danger)] hover:bg-[var(--danger-bg)] dark:hover:bg-[var(--danger)]/10 transition-colors">
@@ -1323,14 +1323,14 @@ export function ScheduleSection({ doctorUuid, clinicUuid, readOnly = false }: {
return (
-
برنامه کاری
-
+
برنامه کاری
+
{SCHEDULE_TABS.map(t => (
setTab(t.id)}
className={`flex-1 text-xs sm:text-sm font-medium px-2 py-1.5 rounded-lg transition-colors ${
tab === t.id
- ? 'bg-white dark:bg-gray-700 text-slate-800 dark:text-slate-100 shadow-sm'
- : 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200'
+ ? 'bg-[var(--surface)] dark:bg-[var(--surface-2)] text-[var(--text)] dark:text-[var(--text)] shadow-sm'
+ : 'text-[var(--text-2)] dark:text-[var(--text-3)] hover:text-[var(--text)] dark:hover:text-[var(--text)]'
}`}>
{t.label}
diff --git a/assets/admin/components/session/CreateStep.tsx b/assets/admin/components/session/CreateStep.tsx
index dcdbaf18..4ad07c11 100644
--- a/assets/admin/components/session/CreateStep.tsx
+++ b/assets/admin/components/session/CreateStep.tsx
@@ -38,9 +38,9 @@ const nowHHMM = () => {
const toSessionAt = (iso: string, time: string) => Math.floor(new Date(`${iso}T${time || '12:00'}:00`).getTime() / 1000);
// برچسب فیلد — tauri Step1Details Typography (fontWeight 500, 0.875rem, #6B7280)
-const fieldLabel: React.CSSProperties = { fontWeight: 500, lineHeight: '130%', fontSize: '0.875rem', marginBottom: 8, color: '#6B7280', display: 'block' };
-const primaryBtn: React.CSSProperties = { background: '#5559CE', color: '#fff', border: 'none', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
-const ghostBtn: React.CSSProperties = { background: 'transparent', color: '#5559CE', border: '1px solid #5559CE', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
+const fieldLabel: React.CSSProperties = { fontWeight: 500, lineHeight: '130%', fontSize: '0.875rem', marginBottom: 8, color: 'var(--text-2)', display: 'block' };
+const primaryBtn: React.CSSProperties = { background: 'var(--primary)', color: 'var(--on-primary)', border: 'none', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
+const ghostBtn: React.CSSProperties = { background: 'transparent', color: 'var(--primary)', border: '1px solid var(--primary)', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
interface Props {
recordUuid: string;
@@ -338,13 +338,13 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel, e
/** ردیف شمارندهی tauri (باکس ۷۴px، +/− نارنجی #FF7A45، سطل روی تعداد ۱) */
const counter = (qty: number, set: (q: number) => void) => (
-
-
set(qty + 1)} style={{ border: 'none', background: 'transparent', color: '#FF7A45', cursor: 'pointer', display: 'flex' }}>
+
+
set(qty + 1)} style={{ border: 'none', background: 'transparent', color: 'var(--accent)', cursor: 'pointer', display: 'flex' }}>
-
{qty}
-
set(qty - 1)} style={{ border: 'none', background: 'transparent', color: '#FF7A45', cursor: 'pointer', display: 'flex' }}>
- {qty > 1 ? : }
+ {qty}
+ set(qty - 1)} style={{ border: 'none', background: 'transparent', color: 'var(--accent)', cursor: 'pointer', display: 'flex' }}>
+ {qty > 1 ? : }
);
@@ -353,8 +353,8 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel, e
{/* پذیرش کننده — tauri UserTick + کاربر لاگینشده */}
-
- پذیرش کننده: {userName || '—'}
+
+ پذیرش کننده: {userName || '—'}
{/* تاریخ و ساعت پذیرش */}
@@ -365,8 +365,8 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel, e
ساعت پذیرش
-
-
+
+
setItemUuid(v ? String(v) : '')} placeholder="جستجو و انتخاب سرویس..." />
-
-
+
+
قیمت: {currentItem ? formatRial(currentItem.price_rials) : '—'}
-
-
+
+
{/* خدمات انتخاب شده */}
{selectedServices.length > 0 && (
-
-
خدمات انتخاب شده
+
+
خدمات انتخاب شده
{selectedServices.map(item => (
- {item.name}
+ {item.name}
{counter(item.qty, (q) => setServiceQty(item.uuid, q))}
- قیمت: {formatRial(item.price * item.qty)}
+ قیمت: {formatRial(item.price * item.qty)}
))}
-
مجموع قیمت خدمات: {formatRial(servicesTotal)}
+
مجموع قیمت خدمات: {formatRial(servicesTotal)}
)}
@@ -425,36 +425,36 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel, e
{/* انتخاب کالای مصرفی */}
-
انتخاب کالای مصرفی
+
انتخاب کالای مصرفی
setConsumableUuid(v ? String(v) : '')} placeholder="انتخاب کنید..." />
-
-
+
+
قیمت: {currentConsumable ? formatRial(currentConsumable.price) : '—'}
-
-
+
+
{/* کالاهای انتخاب شده — باکس خطچین tauri */}
-
-
کالاهای انتخاب شده
+
+
کالاهای انتخاب شده
{selectedConsumables.length === 0 ? (
-
کالایی انتخاب نشده است
+
کالایی انتخاب نشده است
) : selectedConsumables.map(item => (
- {item.name}
+ {item.name}
{counter(item.qty, (q) => setConsumableQty(item.uuid, q))}
- قیمت: {formatRial(item.price * item.qty)}
+ قیمت: {formatRial(item.price * item.qty)}
))}
-
مجموع قیمت کالاها: {formatRial(consumablesTotal)}
+
مجموع قیمت کالاها: {formatRial(consumablesTotal)}
{/* انتخاب پکیج */}
@@ -527,7 +527,7 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel, e
{/* خلاصه مبلغ */}
-
+
ویزیت {formatRial(afterSupp)}
{servicesTotal > 0 &&
سهم بیمار خدمات {formatRial(servicesPatient)}
}
{consumablesTotal > 0 &&
کالاهای مصرفی {formatRial(consumablesTotal)}
}
diff --git a/assets/admin/components/session/DetailsStep.tsx b/assets/admin/components/session/DetailsStep.tsx
index 737169f3..14c2b2e8 100644
--- a/assets/admin/components/session/DetailsStep.tsx
+++ b/assets/admin/components/session/DetailsStep.tsx
@@ -4,8 +4,8 @@ import type { SessionCardData } from '../SessionServiceCard';
import { METHOD_LABELS } from './PaymentStep';
import { useIssueInvoice } from '../../hooks/useIssueInvoice';
-const primaryBtn: React.CSSProperties = { background: '#5559CE', color: '#fff', border: 'none', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
-const ghostBtn: React.CSSProperties = { background: 'transparent', color: '#5559CE', border: '1px solid #5559CE', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
+const primaryBtn: React.CSSProperties = { background: 'var(--primary)', color: 'var(--on-primary)', border: 'none', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
+const ghostBtn: React.CSSProperties = { background: 'transparent', color: 'var(--primary)', border: '1px solid var(--primary)', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
interface Props {
session: SessionCardData;
@@ -69,82 +69,82 @@ export default function DetailsStep({ session, recordUuid, onBack, onFinish }: P
return (
<>
{/* گام جزییات — پورت tauri Step3Final با دیتای واقعی */}
-
+
{visitAt && (
- تاریخ و ساعت مراجعه:
- {formatDateTime(visitAt)}
+ تاریخ و ساعت مراجعه:
+ {formatDateTime(visitAt)}
)}
-
+
{serviceNames.length ? serviceNames.join(' - ') : 'ویزیت'}
-
- {session.doctor_name || '—'}
+
+ {session.doctor_name || '—'}
{costItems.length > 0 && (
-
+
{costItems.map((it, i) => (
- {it.label}
- {formatRial(it.rials)}
+ {it.label}
+ {formatRial(it.rials)}
))}
{hasInsurance && (
<>
-
-
جمع کل خدمات
-
{formatRial(grossTotal)}
+
+ جمع کل خدمات
+ {formatRial(grossTotal)}
{baseInsurance > 0 && (
- سهم بیمه پایه
- {formatRial(baseInsurance)}
+ سهم بیمه پایه
+ {formatRial(baseInsurance)}
)}
{suppInsurance > 0 && (
- سهم بیمه تکمیلی
- {formatRial(suppInsurance)}
+ سهم بیمه تکمیلی
+ {formatRial(suppInsurance)}
)}
>
)}
-
-
{hasInsurance ? 'سهم بیمار' : 'جمع کل'}
-
{formatRial(finalPrice)}
+
+ {hasInsurance ? 'سهم بیمار' : 'جمع کل'}
+ {formatRial(finalPrice)}
)}
- هزینه سرویس:
- {formatRial(finalPrice)}
+ هزینه سرویس:
+ {formatRial(finalPrice)}
-
+
- تخفیف:
- {formatRial(discountRials)}
+ تخفیف:
+ {formatRial(discountRials)}
-
پرداخت شده ها:
+
پرداخت شده ها:
{payments.length === 0 ? (
-
پرداختی ثبت نشده است
+
پرداختی ثبت نشده است
) : payments.map((p) => (
-
+
-
- {METHOD_LABELS[p.method] ?? p.method}
+
+ {METHOD_LABELS[p.method] ?? p.method}
- مبلغ: {formatRial(p.amount_rials)}
+ مبلغ: {formatRial(p.amount_rials)}
-
+
{(p.paid_at ?? p.created_at) && {formatDateTime(p.paid_at ?? p.created_at!)} }
{p.created_by_name && ثبت: {p.created_by_name} }
@@ -153,8 +153,8 @@ export default function DetailsStep({ session, recordUuid, onBack, onFinish }: P
- مبلغ باقی مانده:
- {formatRial(debt)}
+ مبلغ باقی مانده:
+ {formatRial(debt)}
diff --git a/assets/admin/components/session/PaymentStep.tsx b/assets/admin/components/session/PaymentStep.tsx
index 64f1bc9a..fa2386da 100644
--- a/assets/admin/components/session/PaymentStep.tsx
+++ b/assets/admin/components/session/PaymentStep.tsx
@@ -30,9 +30,9 @@ const todayISO = () => new Date().toISOString().slice(0, 10);
/** YYYY-MM-DD → unix (ظهر همان روز تا با هر timezone یک روز بماند) */
const isoToUnix = (iso: string) => Math.floor(new Date(`${iso}T12:00:00`).getTime() / 1000);
-const fieldLabel: React.CSSProperties = { fontSize: 14, color: '#6B7280', marginBottom: 8, display: 'block' };
-const primaryBtn: React.CSSProperties = { background: '#5559CE', color: '#fff', border: 'none', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
-const ghostBtn: React.CSSProperties = { background: 'transparent', color: '#5559CE', border: '1px solid #5559CE', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
+const fieldLabel: React.CSSProperties = { fontSize: 14, color: 'var(--text-2)', marginBottom: 8, display: 'block' };
+const primaryBtn: React.CSSProperties = { background: 'var(--primary)', color: 'var(--on-primary)', border: 'none', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
+const ghostBtn: React.CSSProperties = { background: 'transparent', color: 'var(--primary)', border: '1px solid var(--primary)', borderRadius: 4, height: 46, fontSize: 14, fontWeight: 500, cursor: 'pointer' };
interface Props {
recordUuid: string;
@@ -146,26 +146,26 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
{/* هزینه سرویس */}
- هزینه سرویس:
- {formatRial(finalPrice)}
+ هزینه سرویس:
+ {formatRial(finalPrice)}
{/* تخفیفهای پیشنهادی (قوانین تخفیف) */}
{suggestions.length > 0 && (
-
-
تخفیفهای قابل اعمال:
+
+
تخفیفهای قابل اعمال:
{suggestions.map((s) => (
-
- {s.rule_name} — {formatRial(s.discount_rials)}
- {' '}({s.discount_type === 'percent' ? `${s.value}٪` : 'مبلغ ثابت'})
+
+ {s.rule_name} — {formatRial(s.discount_rials)}
+ {' '}({s.discount_type === 'percent' ? `${s.value}٪` : 'مبلغ ثابت'})
applyRule(s.rule_uuid)}
disabled={discountMut.isPending}
- style={{ height: 32, minWidth: 64, border: 'none', borderRadius: 6, background: '#5559CE', color: '#fff', fontSize: 12.5, fontWeight: 600, cursor: 'pointer' }}
+ style={{ height: 32, minWidth: 64, border: 'none', borderRadius: 6, background: 'var(--primary)', color: 'var(--on-primary)', fontSize: 12.5, fontWeight: 600, cursor: 'pointer' }}
>
اعمال
@@ -178,8 +178,8 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
{/* تخفیف — port of tauri DiscountInput (نوع + مقدار + ثبت) */}
تخفیف:
-
-
+
+
ثبت
@@ -207,64 +207,64 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
onClick={removeDiscount}
style={{ display: 'flex', alignItems: 'center', gap: 4, flexShrink: 0, cursor: 'pointer', marginBottom: 16, minWidth: 120, justifyContent: 'center' }}
>
-
- حذف تخفیف
+
+ حذف تخفیف
{/* خلاصهی مبلغ: قبل از تخفیف / تخفیف / نهایی / منبع */}
-
+
{hasInsurance && (
<>
- هزینه کل خدمات:
- {formatRial(grossTotal)}
+ هزینه کل خدمات:
+ {formatRial(grossTotal)}
{baseInsurance > 0 && (
- سهم بیمه پایه:
- {formatRial(baseInsurance)}
+ سهم بیمه پایه:
+ {formatRial(baseInsurance)}
)}
{suppInsurance > 0 && (
- سهم بیمه تکمیلی:
- {formatRial(suppInsurance)}
+ سهم بیمه تکمیلی:
+ {formatRial(suppInsurance)}
)}
>
)}
- {hasInsurance ? 'سهم بیمار:' : 'مبلغ قبل از تخفیف:'}
- {formatRial(finalPrice)}
+ {hasInsurance ? 'سهم بیمار:' : 'مبلغ قبل از تخفیف:'}
+ {formatRial(finalPrice)}
- مبلغ تخفیف{appliedRuleLabel ? ` (${appliedRuleLabel})` : ''}:
- {formatRial(discountRials)}
+ مبلغ تخفیف{appliedRuleLabel ? ` (${appliedRuleLabel})` : ''}:
+ {formatRial(discountRials)}
-
-
مبلغ نهایی قابل پرداخت:
-
{formatRial(payable)}
+
+ مبلغ نهایی قابل پرداخت:
+ {formatRial(payable)}
{/* تاریخ پرداخت */}
-
تاریخ پرداخت
+
تاریخ پرداخت
{/* روشهای پرداخت */}
-
انتخاب روش های پرداخت:
+
انتخاب روش های پرداخت:
- موجودی کیف پول:
+ موجودی کیف پول:
-
{formatRial(walletBalance)}
+
{formatRial(walletBalance)}
- مبلغ باقیمانده:
- {formatRial(debt)}
+ مبلغ باقیمانده:
+ {formatRial(debt)}
@@ -273,15 +273,15 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
{METHODS.map((m) => {
const open = expanded === m.key;
return (
-
+
{ setExpanded(open ? null : m.key); setAmount(open ? 0 : defaultAmountFor(m.key)); }}
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', padding: '14px 4px', background: 'transparent', border: 'none', cursor: 'pointer' }}
>
- {m.label}
-
+ {m.label}
+
{open && (
@@ -309,34 +309,34 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
{/* پرداخت شدهها — باکس خطچین tauri */}
-
-
پرداخت شده ها:
+
+
پرداخت شده ها:
{payments.length === 0 ? (
-
پرداختی ثبت نشده است
+
پرداختی ثبت نشده است
) : (
{payments.map((p) => (
-
+
-
- {METHOD_LABELS[p.method] ?? p.method}
+
+ {METHOD_LABELS[p.method] ?? p.method}
-
مبلغ : {formatRial(p.amount_rials)}
+
مبلغ : {formatRial(p.amount_rials)}
{p.method !== 'wallet' && (
setEditPay({ uuid: p.uuid, method: p.method, amountToman: rialToToman(p.amount_rials) })}
- style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: '#5559ce', display: 'flex' }}>
+ style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: 'var(--primary)', display: 'flex' }}>
setDelPayUuid(p.uuid)}
- style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: '#EF4444', display: 'flex' }}>
+ style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: 'var(--danger)', display: 'flex' }}>
)}
-
+
{(p.paid_at ?? p.created_at) && {formatDateTime(p.paid_at ?? p.created_at!)} }
{p.created_by_name && ثبت: {p.created_by_name} }
@@ -345,8 +345,8 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
)}
- مبلغ باقیمانده :
- {formatRial(debt)}
+ مبلغ باقیمانده :
+ {formatRial(debt)}
diff --git a/assets/admin/components/session/SessionAuditModal.tsx b/assets/admin/components/session/SessionAuditModal.tsx
index 627cdb3a..30d64ff4 100644
--- a/assets/admin/components/session/SessionAuditModal.tsx
+++ b/assets/admin/components/session/SessionAuditModal.tsx
@@ -16,9 +16,9 @@ const FIELD_LABELS: Record
= {
};
const OP_LABELS: Record = {
- create: { label: 'ایجاد', color: '#3c9a4f' },
- update: { label: 'ویرایش', color: '#5559ce' },
- delete: { label: 'حذف', color: '#EF4444' },
+ create: { label: 'ایجاد', color: 'var(--success)' },
+ update: { label: 'ویرایش', color: 'var(--primary)' },
+ delete: { label: 'حذف', color: 'var(--danger)' },
};
// مقادیر پولی (ریال) را با فرمت تومان نشان بده؛ بقیه را خام.
@@ -53,7 +53,7 @@ export default function SessionAuditModal({ sessionUuid, onClose }: { sessionUui
- {op.label}
+ {op.label}
{FIELD_LABELS[r.field] ?? r.field}
{formatDateTime(r.created_at)}
diff --git a/assets/admin/components/ui/AppointmentStatusDropdown.tsx b/assets/admin/components/ui/AppointmentStatusDropdown.tsx
index 92e0023a..d820b22d 100644
--- a/assets/admin/components/ui/AppointmentStatusDropdown.tsx
+++ b/assets/admin/components/ui/AppointmentStatusDropdown.tsx
@@ -8,15 +8,15 @@ import ConfirmAppointmentModal from '../appointments/ConfirmAppointmentModal';
// Labels follow the Figma نوبتها design (ثبت شده / قطعی شده / ویزیت شده …).
export const STATUS_META: Record
= {
- pending: { label: 'ثبت شده', color: '#3b82f6' },
- confirmed: { label: 'قطعی شده', color: '#14b8a6' },
- following_up: { label: 'در حال پیگیری', color: '#f59e0b' },
- salon: { label: 'سالن', color: '#8b5cf6' },
- completed: { label: 'ویزیت شده', color: '#22c55e' },
- cancelled_by_doctor: { label: 'لغو شده', color: '#ef4444' },
- cancelled_by_user: { label: 'لغو توسط بیمار', color: '#ef4444' },
- no_show: { label: 'غیبت', color: '#9ca3af' },
- expired: { label: 'منقضی شده', color: '#9ca3af' },
+ pending: { label: 'ثبت شده', color: 'var(--info)' },
+ confirmed: { label: 'قطعی شده', color: 'var(--primary)' },
+ following_up: { label: 'در حال پیگیری', color: 'var(--warning)' },
+ salon: { label: 'سالن', color: 'var(--violet)' },
+ completed: { label: 'ویزیت شده', color: 'var(--success)' },
+ cancelled_by_doctor: { label: 'لغو شده', color: 'var(--danger)' },
+ cancelled_by_user: { label: 'لغو توسط بیمار', color: 'var(--danger)' },
+ no_show: { label: 'غیبت', color: 'var(--text-3)' },
+ expired: { label: 'منقضی شده', color: 'var(--text-3)' },
};
// Mirrors Appointment::ALLOWED_TRANSITIONS on the backend.
@@ -83,7 +83,7 @@ export default function AppointmentStatusDropdown({ uuid, currentStatus, version
onError: (e: any) => toast.error(e?.message || 'خطا در تغییر وضعیت'),
});
- const meta = STATUS_META[currentStatus] ?? { label: currentStatus, color: '#9ca3af' };
+ const meta = STATUS_META[currentStatus] ?? { label: currentStatus, color: 'var(--text-3)' };
const nextStatuses = TRANSITIONS[currentStatus] ?? [];
function handleToggle() {
@@ -142,7 +142,7 @@ export default function AppointmentStatusDropdown({ uuid, currentStatus, version
}}
>
{nextStatuses.map(s => {
- const sm = STATUS_META[s] ?? { label: s, color: '#9ca3af' };
+ const sm = STATUS_META[s] ?? { label: s, color: 'var(--text-3)' };
return (
{ if (!sel && !tod) (e.currentTarget as HTMLButtonElement).style.background = 'var(--surface-2)'; }}
@@ -216,7 +216,7 @@ export default function PersianCalendar({ value, onChange, onClose, enableYearPi
key={m}
type="button"
onClick={() => { setViewMonth(m); setMode('day'); }}
- style={{ ...gridItemStyle, background: active ? 'var(--primary)' : 'transparent', color: active ? '#fff' : 'var(--text)' }}
+ style={{ ...gridItemStyle, background: active ? 'var(--primary)' : 'transparent', color: active ? 'var(--on-primary)' : 'var(--text)' }}
onMouseEnter={e => { if (!active) (e.currentTarget as HTMLButtonElement).style.background = 'var(--surface-2)'; }}
onMouseLeave={e => { if (!active) (e.currentTarget as HTMLButtonElement).style.background = 'transparent'; }}
>
@@ -237,7 +237,7 @@ export default function PersianCalendar({ value, onChange, onClose, enableYearPi
key={y}
type="button"
onClick={() => { setViewYear(y); setMode('month'); }}
- style={{ ...gridItemStyle, background: active ? 'var(--primary)' : 'transparent', color: active ? '#fff' : 'var(--text)' }}
+ style={{ ...gridItemStyle, background: active ? 'var(--primary)' : 'transparent', color: active ? 'var(--on-primary)' : 'var(--text)' }}
onMouseEnter={e => { if (!active) (e.currentTarget as HTMLButtonElement).style.background = 'var(--surface-2)'; }}
onMouseLeave={e => { if (!active) (e.currentTarget as HTMLButtonElement).style.background = 'transparent'; }}
>
diff --git a/assets/admin/components/ui/StatCard.tsx b/assets/admin/components/ui/StatCard.tsx
index 79693f06..93a1dc72 100644
--- a/assets/admin/components/ui/StatCard.tsx
+++ b/assets/admin/components/ui/StatCard.tsx
@@ -33,7 +33,7 @@ export default function StatCard({ tone, label, value, icon }: Props) {
diff --git a/assets/admin/components/ui/appointmentStatusMeta.test.ts b/assets/admin/components/ui/appointmentStatusMeta.test.ts
index 08385223..7da73a52 100644
--- a/assets/admin/components/ui/appointmentStatusMeta.test.ts
+++ b/assets/admin/components/ui/appointmentStatusMeta.test.ts
@@ -23,7 +23,7 @@ describe('STATUS_META', () => {
it.each(BACKEND_STATUSES)('has a Persian label and colour for %s', (status) => {
expect(STATUS_META[status]).toBeDefined();
expect(STATUS_META[status].label.trim()).not.toBe('');
- expect(STATUS_META[status].color).toMatch(/^#[0-9a-f]{6}$/i);
+ expect(STATUS_META[status].color).toMatch(/^var\(--[a-z0-9-]+\)$/);
});
it('carries no label that is still English', () => {
diff --git a/assets/admin/pages/AppointmentCreatePage.tsx b/assets/admin/pages/AppointmentCreatePage.tsx
index 08310577..00946c24 100644
--- a/assets/admin/pages/AppointmentCreatePage.tsx
+++ b/assets/admin/pages/AppointmentCreatePage.tsx
@@ -352,7 +352,7 @@ export default function AppointmentCreatePage() {
border: active ? '1px solid var(--primary)' : '1px solid var(--border-2)',
background: active ? 'var(--primary)' : 'transparent',
}}>
- {active &&
}
+ {active &&
}
{o.name}
@@ -378,7 +378,7 @@ export default function AppointmentCreatePage() {
removeService(s.uuid)}
style={{
display: 'grid', placeItems: 'center', width: 16, height: 16, borderRadius: 999,
- border: 'none', cursor: 'pointer', background: 'var(--primary)', color: '#fff',
+ border: 'none', cursor: 'pointer', background: 'var(--primary)', color: 'var(--on-primary)',
fontSize: 12, lineHeight: 1, fontFamily: 'inherit',
}}>×
@@ -451,13 +451,13 @@ export default function AppointmentCreatePage() {
setDepositRequired(e.target.checked)}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', margin: 0, opacity: 0, cursor: 'pointer' }} />
بیعانه مورد نیاز است.
diff --git a/assets/admin/pages/AppointmentDetailPage.tsx b/assets/admin/pages/AppointmentDetailPage.tsx
index ae359e83..cae5bcff 100644
--- a/assets/admin/pages/AppointmentDetailPage.tsx
+++ b/assets/admin/pages/AppointmentDetailPage.tsx
@@ -123,7 +123,7 @@ export default function AppointmentDetailPage() {
]}
action={
navigate(backTo)}
- className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-100 transition-colors">
+ className="flex items-center gap-2 text-sm text-[var(--text-2)] hover:text-[var(--text)] transition-colors">
بازگشت
@@ -138,8 +138,8 @@ export default function AppointmentDetailPage() {
) : appt ? (
-
-
اطلاعات بیمار
+
+
اطلاعات بیمار
{appt.patient_mobile}} />
@@ -151,10 +151,10 @@ export default function AppointmentDetailPage() {
-
-
وضعیت و اقدامات
+
+
وضعیت و اقدامات
-
وضعیت فعلی:
+
وضعیت فعلی:
@@ -189,37 +189,37 @@ export default function AppointmentDetailPage() {
-
+
setCancelOpen(true)}
- className="w-full py-2 border border-red-300 text-red-600 text-sm rounded-[10px] hover:bg-red-50 transition-colors"
+ className="w-full py-2 border border-[var(--danger)] text-[var(--danger)] text-sm rounded-[10px] hover:bg-[var(--danger-bg)] transition-colors"
>
لغو نوبت
-
-
تاریخچه رویدادها
+
+
تاریخچه رویدادها
{eventsQuery.isLoading ? (
) : events.length === 0 ? (
-
رویدادی برای این نوبت ثبت نشده است.
+
رویدادی برای این نوبت ثبت نشده است.
) : (
{events.map((ev, i) => (
-
+
- {ev.title}
- {formatDateTime(ev.created_at)}
+ {ev.title}
+ {formatDateTime(ev.created_at)}
{ev.actor_name && (
-
توسط: {ev.actor_name}
+
توسط: {ev.actor_name}
)}
{ev.reason && (
-
دلیل: {ev.reason}
+
دلیل: {ev.reason}
)}
@@ -229,7 +229,7 @@ export default function AppointmentDetailPage() {
) : (
-
+
نوبتی یافت نشد
)}
diff --git a/assets/admin/pages/AppointmentsPage.tsx b/assets/admin/pages/AppointmentsPage.tsx
index 03ae3717..95e01205 100644
--- a/assets/admin/pages/AppointmentsPage.tsx
+++ b/assets/admin/pages/AppointmentsPage.tsx
@@ -75,7 +75,7 @@ function DateNavigator({ date, onChange }: { date: string; onChange: (d: string)
background: 'var(--surface)', border: '1px solid var(--border)',
borderRadius: 'var(--r-sm)', minWidth: 130, gap: 1,
}}>
-
+
{weekDay}
diff --git a/assets/admin/pages/BlogFormPage.tsx b/assets/admin/pages/BlogFormPage.tsx
index d8e2fd97..bb0306cc 100644
--- a/assets/admin/pages/BlogFormPage.tsx
+++ b/assets/admin/pages/BlogFormPage.tsx
@@ -142,7 +142,7 @@ export default function BlogFormPage() {
عنوان مقاله *
- {errors.title &&
{errors.title.message}
}
+ {errors.title &&
{errors.title.message}
}
@@ -170,7 +170,7 @@ export default function BlogFormPage() {
)}
/>
- {errors.body && {errors.body.message}
}
+ {errors.body && {errors.body.message}
}
diff --git a/assets/admin/pages/CategoriesPage.tsx b/assets/admin/pages/CategoriesPage.tsx
index 72c9c8f6..ca1cc95b 100644
--- a/assets/admin/pages/CategoriesPage.tsx
+++ b/assets/admin/pages/CategoriesPage.tsx
@@ -90,7 +90,7 @@ function LogoUploadField({ value, onChange, uploadUrl }: {
onChange(null)} style={{
position: 'absolute', top: -6, right: -6, width: 20, height: 20, borderRadius: 10,
- background: 'var(--danger)', color: '#fff', border: 'none', cursor: 'pointer',
+ background: 'var(--danger)', color: 'var(--on-primary)', border: 'none', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
diff --git a/assets/admin/pages/ClinicDetailPage.tsx b/assets/admin/pages/ClinicDetailPage.tsx
index 1f2fedfc..c1f17010 100644
--- a/assets/admin/pages/ClinicDetailPage.tsx
+++ b/assets/admin/pages/ClinicDetailPage.tsx
@@ -631,7 +631,7 @@ export default function ClinicDetailPage() {
borderRadius: '50%', background: 'var(--primary)', border: '2px solid var(--surface)',
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
}}>
-
+
e.target.files?.[0] && handleLogoUpload(e.target.files[0])} />
diff --git a/assets/admin/pages/ClinicServicesPage.tsx b/assets/admin/pages/ClinicServicesPage.tsx
index 67c87766..5f1a1721 100644
--- a/assets/admin/pages/ClinicServicesPage.tsx
+++ b/assets/admin/pages/ClinicServicesPage.tsx
@@ -35,7 +35,7 @@ function Avatar({ name }: { name: string }) {
width: 26, height: 26, borderRadius: '50%',
background: 'linear-gradient(145deg, oklch(0.62 0.15 162), oklch(0.48 0.16 162))',
display: 'flex', alignItems: 'center', justifyContent: 'center',
- fontSize: 11, fontWeight: 700, color: '#fff', flexShrink: 0,
+ fontSize: 11, fontWeight: 700, color: 'var(--on-primary)', flexShrink: 0,
}}>
{name.charAt(0)}
diff --git a/assets/admin/pages/DashboardPage.tsx b/assets/admin/pages/DashboardPage.tsx
index 8660851e..29cca6e7 100644
--- a/assets/admin/pages/DashboardPage.tsx
+++ b/assets/admin/pages/DashboardPage.tsx
@@ -66,7 +66,7 @@ const PAY_LABEL: Record = {
pending: 'در انتظار', received: 'موفق', canceled: 'لغو شده', refund: 'استرداد',
};
const PAY_COLOR: Record = {
- pending: '#f59e0b', received: '#22c55e', canceled: '#ef4444', refund: '#3b82f6',
+ pending: 'var(--warning)', received: 'var(--success)', canceled: 'var(--danger)', refund: 'var(--info)',
};
const PAY_CLS: Record = {
pending: 'amber', received: 'green', canceled: 'red', refund: 'blue',
@@ -373,7 +373,7 @@ function AdminDashboard() {
const revSeries = useMemo(() => charts?.revenue_by_day?.map(d => d.amount_rials) ?? [], [charts]);
const donutData = useMemo(() =>
(charts?.appointment_status ?? []).slice(0, 7).map(s => ({
- label: APPT_LABEL[s.status] ?? s.status, value: s.count, color: APPT_COLOR[s.status] ?? '#94a3b8',
+ label: APPT_LABEL[s.status] ?? s.status, value: s.count, color: APPT_COLOR[s.status] ?? 'var(--text-3)',
})), [charts]);
const hbarsData = useMemo(() => (charts?.top_specialties ?? []).map(s => ({ label: s.name, value: s.count })), [charts]);
@@ -400,13 +400,13 @@ function AdminDashboard() {
if (!recent) return [];
const evs: { title: string; sub: string; time: string; color: string }[] = [];
(recent.appointments ?? []).slice(0, 4).forEach(a => {
- evs.push({ title: `نوبت ${APPT_LABEL[a.status] ?? a.status}`, sub: `${a.user_name || a.user_mobile} · ${a.doctor_name}`, time: a.created_at, color: APPT_COLOR[a.status] ?? '#94a3b8' });
+ evs.push({ title: `نوبت ${APPT_LABEL[a.status] ?? a.status}`, sub: `${a.user_name || a.user_mobile} · ${a.doctor_name}`, time: a.created_at, color: APPT_COLOR[a.status] ?? 'var(--text-3)' });
});
(recent.payments ?? []).slice(0, 3).forEach(p => {
- evs.push({ title: `پرداخت ${PAY_LABEL[p.status] ?? p.status}`, sub: `${p.user_name || p.user_mobile} · ${formatRial(p.amount)}`, time: p.created_at, color: PAY_COLOR[p.status] ?? '#94a3b8' });
+ evs.push({ title: `پرداخت ${PAY_LABEL[p.status] ?? p.status}`, sub: `${p.user_name || p.user_mobile} · ${formatRial(p.amount)}`, time: p.created_at, color: PAY_COLOR[p.status] ?? 'var(--text-3)' });
});
(recent.users ?? []).slice(0, 3).forEach(u => {
- evs.push({ title: 'ثبتنام کاربر جدید', sub: u.name || u.mobile, time: u.created_at, color: '#8b5cf6' });
+ evs.push({ title: 'ثبتنام کاربر جدید', sub: u.name || u.mobile, time: u.created_at, color: 'var(--violet)' });
});
return evs.sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime()).slice(0, 8);
}, [recent]);
diff --git a/assets/admin/pages/DoctorDetailPage.tsx b/assets/admin/pages/DoctorDetailPage.tsx
index 9511001d..b42873cc 100644
--- a/assets/admin/pages/DoctorDetailPage.tsx
+++ b/assets/admin/pages/DoctorDetailPage.tsx
@@ -90,8 +90,8 @@ const AVATAR_COLORS = [
'from-amber-500 to-orange-600',
];
const SPECIALTY_COLORS = [
- 'bg-red-100 dark:bg-red-400/10 text-red-700 dark:text-red-300',
- 'bg-blue-100 dark:bg-blue-400/10 text-blue-700 dark:text-blue-300',
+ 'bg-[var(--danger-bg)] dark:bg-red-400/10 text-[var(--danger)] dark:text-red-300',
+ 'bg-[var(--info-bg)] dark:bg-blue-400/10 text-[var(--info)] dark:text-blue-300',
'bg-emerald-100 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300',
'bg-violet-100 dark:bg-violet-400/10 text-violet-700 dark:text-violet-300',
'bg-orange-100 dark:bg-orange-400/10 text-orange-700 dark:text-orange-300',
@@ -136,13 +136,13 @@ function DoctorAvatar({ name, img, idx, onUpload, uploading }: {
>
{img
?
- : {initials}
+ : {initials}
}
{onUpload && (
{uploading
- ?
- :
+ ?
+ :
}
)}
@@ -161,19 +161,19 @@ function InfoCard({ icon: Icon, label, value, mono = false, copyable = false }:
mono?: boolean; copyable?: boolean;
}) {
return (
-
-
-
+
+
+
-
{label}
-
- {value ?? ثبت نشده }
+
{label}
+
+ {value ?? ثبت نشده }
{copyable && typeof value === 'string' && (
{ navigator.clipboard.writeText(value); toast.success('کپی شد'); }}
- className="opacity-0 group-hover:opacity-100 w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-600 transition-all shrink-0">
+ className="opacity-0 group-hover:opacity-100 w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--text-2)] transition-all shrink-0">
)}
@@ -187,9 +187,9 @@ function StarRating({ rate }: { rate: number }) {
{Array.from({ length: 5 }).map((_, i) =>
i < Math.round(rate)
?
- :
+ :
)}
-
{rate.toFixed(1)}
+
{rate.toFixed(1)}
);
}
@@ -241,7 +241,7 @@ function MapPicker({ lat, lng, onChange, flyTarget }: {
const pos: [number, number] | null = lat !== null && lng !== null ? [lat, lng] : null;
const center: [number, number] = pos ?? IRAN_CENTER;
return (
-
+
{selected?.label ?? placeholder ?? 'انتخاب کنید'}
@@ -314,26 +314,26 @@ function SearchableSelect({ options, value, onChange, placeholder, disabled = fa
{open && createPortal(
-
+ className="bg-[var(--surface)] border border-[var(--border)] rounded-xl shadow-2xl overflow-hidden">
+
setQ(e.target.value)}
placeholder="جستجو..." className="cp-input text-sm h-8" />
{ onChange(null, null); setOpen(false); setQ(''); }}
- className="w-full text-right px-3 py-2 text-sm text-slate-400 hover:bg-slate-50 dark:hover:bg-gray-800">
+ className="w-full text-right px-3 py-2 text-sm text-[var(--text-3)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)]">
{placeholder ?? 'انتخاب کنید'}
{filtered.map(o => (
{ onChange(o.value, o.label); setOpen(false); setQ(''); }}
- className={`w-full text-right px-3 py-2 text-sm hover:bg-slate-50 dark:hover:bg-gray-800 ${value === o.value ? 'text-primary-600 dark:text-primary-400 font-medium bg-primary-50 dark:bg-primary-500/10' : 'text-slate-700 dark:text-slate-300'}`}>
+ className={`w-full text-right px-3 py-2 text-sm hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] ${value === o.value ? 'text-primary-600 dark:text-primary-400 font-medium bg-primary-50 dark:bg-primary-500/10' : 'text-[var(--text)] dark:text-[var(--text-3)]'}`}>
{o.label}
))}
{filtered.length === 0 && (
-
نتیجهای یافت نشد
+
نتیجهای یافت نشد
)}
,
@@ -379,13 +379,13 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
);
return (
-
-
+
+
setQ(e.target.value)} placeholder="جستجوی تخصص..."
className="cp-input text-sm h-8" />
{selectedLabels.length > 0 && (
-
+
{selectedLabels.map(({ id, name }) => (
{name}
@@ -398,18 +398,18 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
{q ? (
filteredFlat.length > 0
? filteredFlat.map(s => (
-
+
toggleSelect(s.id)}
- className="rounded border-slate-300 dark:border-gray-600 text-primary-600 shrink-0" />
- {s.name}
+ className="rounded border-[var(--border-2)] dark:border-gray-600 text-primary-600 shrink-0" />
+ {s.name}
{s.parent_id !== null && (
-
+
{specialties.find(p => p.id === s.parent_id)?.name}
)}
))
- : نتیجهای یافت نشد
+ : نتیجهای یافت نشد
) : (
parents.map(parent => {
const children = childMap.get(parent.id) ?? [];
@@ -417,16 +417,16 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
const numSel = children.filter(c => selected.includes(c.id)).length;
return (
@@ -487,7 +487,7 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
if (selectedSpecialtyIds.length === 0) {
return (
-
+
ابتدا تخصصهایی را انتخاب کنید تا خدمات نمایش داده شوند
);
@@ -495,31 +495,31 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
if (relevantServices.length === 0) {
return (
-
+
خدمتی برای تخصصهای انتخابشده یافت نشد
);
}
return (
-
+
{tabs.length > 1 && (
-
+
{[{ id: 'all' as const, name: 'همه' }, ...tabs].map(t => (
setActiveTab(t.id as number | 'all')}
- className={`px-2.5 py-1 rounded-lg text-xs font-medium whitespace-nowrap transition-colors ${activeTab === t.id ? 'bg-primary-600 text-white' : 'text-slate-600 dark:text-slate-400 hover:bg-white dark:hover:bg-gray-700'}`}>
+ className={`px-2.5 py-1 rounded-lg text-xs font-medium whitespace-nowrap transition-colors ${activeTab === t.id ? 'bg-primary-600 text-[var(--on-primary)]' : 'text-[var(--text-2)] dark:text-[var(--text-3)] hover:bg-[var(--surface)] dark:hover:bg-[var(--surface-2)]'}`}>
{t.name}
))}
)}
-
+
setQ(e.target.value)} placeholder="جستجوی خدمت..."
className="cp-input text-sm h-8" />
{selectedLabels.length > 0 && (
-
+
{selectedLabels.map(({ id, name }) => (
{name}
@@ -530,14 +530,14 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
)}
{visibleServices.map(svc => (
-
+
toggle(svc.id)}
- className="rounded border-slate-300 dark:border-gray-600 text-emerald-600 shrink-0" />
- {svc.name}
+ className="rounded border-[var(--border-2)] dark:border-gray-600 text-[var(--success)] shrink-0" />
+ {svc.name}
))}
{visibleServices.length === 0 && q && (
- نتیجهای یافت نشد
+ نتیجهای یافت نشد
)}
@@ -644,32 +644,32 @@ function AddressModal({ open, onClose, existing, doctorUuid, onSaved }: {
{/* Row 1: Name + Phone */}
{/* Row 2: Full address */}
-
- آدرس کامل *
+
+ آدرس کامل *
- {errors.address && {errors.address.message}
}
+ {errors.address && {errors.address.message}
}
{/* Row 3: Province + City side by side */}
-
- استان *
+
+ استان *
({ value: p.id, label: p.name }))}
@@ -681,11 +681,11 @@ function AddressModal({ open, onClose, existing, doctorUuid, onSaved }: {
setMapFlyTarget(null);
}}
/>
- {errors.province_id && {errors.province_id.message}
}
+ {errors.province_id && {errors.province_id.message}
}
-
- شهر *
+
+ شهر *
({ value: c.id, label: c.name }))}
@@ -701,23 +701,23 @@ function AddressModal({ open, onClose, existing, doctorUuid, onSaved }: {
}
}}
/>
- {errors.city_id && {errors.city_id.message}
}
+ {errors.city_id && {errors.city_id.message}
}
{/* Row 4: Map */}
-
+
موقعیت روی نقشه
{lat && lng && (
-
+
{lat.toFixed(5)}, {lng.toFixed(5)}
)}
-
+
برای دقت بیشتر روی نقشه کلیک کنید
{ setValue('latitude', ''); setValue('longitude', ''); }}
- className="mt-2 text-xs text-red-500 hover:underline flex items-center gap-1">
+ className="mt-2 text-xs text-[var(--danger)] hover:underline flex items-center gap-1">
حذف موقعیت
)}
@@ -741,26 +741,26 @@ function AddressCard({ addr, onEdit, onDelete, readOnly = false }: {
addr: AddressData; onEdit: () => void; onDelete: () => void; readOnly?: boolean;
}) {
return (
-
-
-
+
+
+
- {addr.name &&
{addr.name}
}
- {addr.address &&
{addr.address}
}
+ {addr.name &&
{addr.name}
}
+ {addr.address &&
{addr.address}
}
{addr.telephone && (
-
+
{addr.telephone}
)}
{addr.province && (
-
+
{addr.province.name}{addr.city ? ` — ${addr.city.name}` : ''}
)}
{addr.map.latitude && addr.map.longitude && (
-
+
{parseFloat(addr.map.latitude).toFixed(4)}, {parseFloat(addr.map.longitude).toFixed(4)}
)}
@@ -769,11 +769,11 @@ function AddressCard({ addr, onEdit, onDelete, readOnly = false }: {
{!readOnly && (
+ className="w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--info)] hover:bg-[var(--info-bg)] dark:hover:bg-blue-500/10 transition-colors">
+ className="w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--danger)] hover:bg-[var(--danger-bg)] dark:hover:bg-[var(--danger)]/10 transition-colors">
@@ -1362,10 +1362,10 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
if (isError || !doctor) {
return (
-
-
+
+
-
پزشک یافت نشد
+
پزشک یافت نشد
navigate('/admin/doctors')} className="cp-btn-secondary mt-2">
بازگشت به لیست پزشکان
@@ -1383,24 +1383,24 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
{/* Breadcrumb */}
-
+
{isOwnProfile ? (
<>
navigate('/admin/dashboard')}
- className="flex items-center gap-1.5 hover:text-slate-800 dark:hover:text-slate-100 transition-colors">
+ className="flex items-center gap-1.5 hover:text-[var(--text)] dark:hover:text-[var(--text)] transition-colors">
داشبورد
/
-
پروفایل من
+
پروفایل من
>
) : (
<>
navigate(primaryRole === 'clinic' ? '/admin/dashboard' : '/admin/doctors')}
- className="flex items-center gap-1.5 hover:text-slate-800 dark:hover:text-slate-100 transition-colors">
+ className="flex items-center gap-1.5 hover:text-[var(--text)] dark:hover:text-[var(--text)] transition-colors">
{primaryRole === 'clinic' ? 'داشبورد' : 'پزشکان'}
/
-
پروفایل پزشک
+
پروفایل پزشک
>
)}
@@ -1422,27 +1422,27 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
/>
)}
{primaryRole !== 'clinic' && !isReadOnly && (
-
کلیک برای تغییر عکس
+
کلیک برای تغییر عکس
)}
-
{displayDoctorName(doctor.name)}
+
{displayDoctorName(doctor.name)}
{/* Active status badge */}
{doctor.active
?
فعال
- :
+ :
غیرفعال
}
{doctor.degree && (
- {degreeLabel}
+ {degreeLabel}
)}
{doctor.gender && (
- {doctor.gender === 'man' ? '♂ مرد' : '♀ زن'}
+ {doctor.gender === 'man' ? '♂ مرد' : '♀ زن'}
)}
{doctor.specialties.length > 0 && (
@@ -1455,7 +1455,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
{doctor.expertise.length > 0 && (
{doctor.expertise.map(e => (
- {e.name}
+ {e.name}
))}
)}
@@ -1477,7 +1477,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
className={`inline-flex items-center gap-1.5 text-sm font-medium px-3 h-10 rounded-xl border transition-colors disabled:opacity-50 ${
doctor.active
? 'border-orange-300 dark:border-orange-500/40 text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-500/10 hover:bg-orange-100 dark:hover:bg-orange-500/20'
- : 'border-emerald-300 dark:border-emerald-500/40 text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 hover:bg-emerald-100 dark:hover:bg-emerald-500/20'
+ : 'border-emerald-300 dark:border-emerald-500/40 text-[var(--success)] dark:text-emerald-400 bg-[var(--success-bg)] dark:bg-emerald-500/10 hover:bg-emerald-100 dark:hover:bg-emerald-500/20'
}`}
>
{toggleMut.isPending
@@ -1495,9 +1495,9 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
{menuOpen && (
-
+
{ setDeleteOpen(true); setMenuOpen(false); }}
- className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10">
+ className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--danger)] dark:text-red-400 hover:bg-[var(--danger-bg)] dark:hover:bg-[var(--danger)]/10">
حذف این پزشک
@@ -1515,7 +1515,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
-
اطلاعات پایه
+
اطلاعات پایه
@@ -1527,8 +1527,8 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
value={
{doctor.representation
- ? navigate(`/admin/representations/${doctor.representation!.uuid}`)}>{doctor.representation.full_name ?? 'نماینده'}
- : بدون نماینده }
+ ? navigate(`/admin/representations/${doctor.representation!.uuid}`)}>{doctor.representation.full_name ?? 'نماینده'}
+ : بدون نماینده }
{isAdmin && (
{ setSelRepId(doctor.representation?.id ?? null); setRepModalOpen(true); }}>
@@ -1543,15 +1543,15 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
{doctor.detail && (
-
بیوگرافی
-
{doctor.detail}
+
بیوگرافی
+
{doctor.detail}
)}
{/* Addresses */}
-
+
آدرسهای مطب ({formatNumber(addresses.length)})
{!isReadOnly && (
@@ -1571,7 +1571,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
))}
) : (
-
+
هنوز آدرسی ثبت نشده است
{!isReadOnly && (
@@ -1590,18 +1590,18 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
{doctor.clinics && doctor.clinics.length > 0 && (
-
+
کلینیکهای مرتبط ({formatNumber(doctor.clinics.length)})
{doctor.clinics.map(c => (
-
-
-
+
+
+
-
{c.name}
- {c.address &&
{c.address}
}
+
{c.name}
+ {c.address &&
{c.address}
}
))}
@@ -1613,7 +1613,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
{/* Sidebar */}
-
+
تخصصها ({formatNumber(doctor.specialties.length)})
{doctor.specialties.length > 0
@@ -1622,7 +1622,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
{s.name}
))}
- :
تخصصی ثبت نشده
+ :
تخصصی ثبت نشده
}
{!isReadOnly && (
setEditOpen(true)} className="w-full mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline text-right">
@@ -1632,16 +1632,16 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
-
+
خدمات ({formatNumber(doctor.expertise.length)})
{doctor.expertise.length > 0
?
{doctor.expertise.map(e => (
- {e.name}
+ {e.name}
))}
- : خدمتی ثبت نشده
+ :
خدمتی ثبت نشده
}
{!isReadOnly && (
setEditOpen(true)} className="w-full mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline text-right">
@@ -1651,30 +1651,30 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
-
امتیاز و رضایت
+
امتیاز و رضایت
-
امتیاز کلی
+
امتیاز کلی
-
درصد رضایت
+
درصد رضایت
-
+
-
{doctor.satisfaction}%
+
{doctor.satisfaction}%
-
UUID
+
UUID
-
{doctor.uuid}
+
{doctor.uuid}
{ navigator.clipboard.writeText(doctor.uuid); toast.success('UUID کپی شد'); }}
- className="w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition-colors shrink-0">
+ className="w-7 h-7 flex items-center justify-center rounded-lg text-[var(--text-3)] hover:text-[var(--text-2)] hover:bg-[var(--surface-2)] transition-colors shrink-0">
@@ -1887,7 +1887,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
>
}
>
-
انتخاب نماینده
+
انتخاب نماینده
{createMut.isPending ? (
-
+
در حال ذخیره...
) : (
diff --git a/assets/admin/pages/EditSessionPage.tsx b/assets/admin/pages/EditSessionPage.tsx
index 8690f1c3..b0f6cac6 100644
--- a/assets/admin/pages/EditSessionPage.tsx
+++ b/assets/admin/pages/EditSessionPage.tsx
@@ -42,20 +42,20 @@ export default function EditSessionPage() {
return (
-
-
nav(-1)} className="bg-white dark:bg-[#222433]" style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer' }}>
+
+
nav(-1)} className="bg-[var(--surface)]" style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer' }}>
بازگشت
پرونده
-
{patientName}
+
{patientName}
-
+
ویرایش مراجعه
nav(-1)}
diff --git a/assets/admin/pages/LoginPage.tsx b/assets/admin/pages/LoginPage.tsx
index b63817e0..bf3ffd61 100644
--- a/assets/admin/pages/LoginPage.tsx
+++ b/assets/admin/pages/LoginPage.tsx
@@ -227,7 +227,7 @@ export default function LoginPage() {
flex: 1, padding: '8px 0', borderRadius: 8, border: 'none', cursor: 'pointer',
fontFamily: 'inherit', fontSize: 13, fontWeight: 600, transition: 'all 0.15s',
background: mode === m ? 'var(--primary)' : 'transparent',
- color: mode === m ? '#fff' : 'var(--text-2)',
+ color: mode === m ? 'var(--on-primary)' : 'var(--text-2)',
}}>
{m === 'password' ? 'رمز عبور' : 'ورود با پیامک'}
diff --git a/assets/admin/pages/MyPatientsPage.tsx b/assets/admin/pages/MyPatientsPage.tsx
index e4110a65..894d1943 100644
--- a/assets/admin/pages/MyPatientsPage.tsx
+++ b/assets/admin/pages/MyPatientsPage.tsx
@@ -901,7 +901,7 @@ function MyPatientsPageInner() {
display: "flex",
alignItems: "center",
justifyContent: "center",
- color: "#fff",
+ color: "var(--on-primary)",
fontWeight: 700,
fontSize: 16,
}}
@@ -1066,11 +1066,11 @@ function MyPatientsPageInner() {
{selectedPatientName}
{hasUnpaid ? (
-
+
تکمیل نشده
) : (
-
+
تکمیل شده
)}
@@ -1089,12 +1089,12 @@ function MyPatientsPageInner() {
{/* وسط: شماره تماس و تاریخ تشکیل پرونده */}
}
+ icon={
}
label="شماره تماس:"
value={
{selectedPatientPhone} }
/>
}
+ icon={
}
label="تاریخ تشکیل پرونده:"
value={formatDate(selectedRecord.created_at)}
/>
@@ -1115,7 +1115,7 @@ function MyPatientsPageInner() {
toast.info("امکان یادداشت بهزودی اضافه میشود")}
style={{
- background: "#F17732", color: "#fff", border: "none", cursor: "pointer",
+ background: "var(--accent)", color: "var(--on-primary)", border: "none", cursor: "pointer",
display: "inline-flex", alignItems: "center", gap: 6,
padding: "8px 16px", borderRadius: 12, fontSize: 15, fontWeight: 500,
}}
@@ -1714,7 +1714,7 @@ function patientAvatar(_name: string, size = 32) {
style={{
width: size, height: size, borderRadius: "50%", flexShrink: 0,
background: "var(--accent, #F17732)",
- color: "#fff", display: "grid", placeItems: "center",
+ color: "var(--on-primary)", display: "grid", placeItems: "center",
}}
>
@@ -1882,8 +1882,8 @@ function VisitSummaryCard({
}}
>
{paid
- ?
- : }
+ ?
+ : }
diff --git a/assets/admin/pages/MyPaymentDetailPage.tsx b/assets/admin/pages/MyPaymentDetailPage.tsx
index 320021a7..a35e0f9c 100644
--- a/assets/admin/pages/MyPaymentDetailPage.tsx
+++ b/assets/admin/pages/MyPaymentDetailPage.tsx
@@ -27,7 +27,7 @@ function Avatar({ name }: { name: string | null }) {
{(name ?? '؟').charAt(0)}
diff --git a/assets/admin/pages/MyPaymentsPage.tsx b/assets/admin/pages/MyPaymentsPage.tsx
index d5c143f5..b6cbc8ac 100644
--- a/assets/admin/pages/MyPaymentsPage.tsx
+++ b/assets/admin/pages/MyPaymentsPage.tsx
@@ -47,7 +47,7 @@ function Avatar({ name }: { name: string | null }) {
{(name ?? '؟').charAt(0)}
diff --git a/assets/admin/pages/MySecretariesPage.tsx b/assets/admin/pages/MySecretariesPage.tsx
index f03709b0..7df09272 100644
--- a/assets/admin/pages/MySecretariesPage.tsx
+++ b/assets/admin/pages/MySecretariesPage.tsx
@@ -16,23 +16,23 @@ import type { Secretary, SecretaryPermissions } from "../types";
const PlusIcon = () => (
-
-
+
+
);
const EyeIcon = () => (
-
-
+
+
);
const EditIcon = () => (
-
-
-
+
+
+
);
@@ -43,7 +43,7 @@ const ChevronDown = ({ open }: { open: boolean }) => (
height="20"
viewBox="0 0 24 24"
fill="none"
- className="text-[#616161] dark:text-[#A1A1A1] transition-transform"
+ className="text-[var(--text-2)] transition-transform"
style={{ transform: open ? "rotate(180deg)" : "rotate(0deg)" }}
>
@@ -56,7 +56,7 @@ function Avatar({ name, size = 32 }: { name?: string; size?: number }) {
const firstLetter = (name?.trim().charAt(0) || "?").toUpperCase();
return (
{firstLetter}
@@ -268,7 +268,7 @@ function PermissionAccordions({
return (
-
+
مجوزهای دسترسی
@@ -278,14 +278,14 @@ function PermissionAccordions({
return (
toggleOpen(section.key)}
className="w-full flex items-center justify-between px-[16px] min-h-[64px] cursor-pointer"
>
-
+
{section.title}
@@ -306,9 +306,9 @@ function PermissionAccordions({
onChange(section.key, item.key, e.target.checked)
}
className="w-[20px] h-[20px] shrink-0"
- style={{ accentColor: "#5559CE", cursor: "pointer" }}
+ style={{ accentColor: "var(--primary)", cursor: "pointer" }}
/>
-
+
{item.label}
@@ -347,9 +347,9 @@ function DefaultTextField({
maxDigits?: number;
}) {
const cls =
- "w-full bg-[#FAFAFA] dark:bg-[#222433] rounded-[8px] border border-[#D7D7D7] dark:border-[#343645] " +
- "text-[#7E7E7E] dark:text-[#D7D8ED] text-[16px] font-normal px-[12px] py-[12.5px] outline-none " +
- "focus:border-[#5559CE] disabled:opacity-70";
+ "w-full bg-[var(--bg)] dark:bg-[var(--surface)] rounded-[8px] border border-[var(--border-2)] dark:border-[var(--border)] " +
+ "text-[var(--text-2)] dark:text-[var(--text)] text-[16px] font-normal px-[12px] py-[12.5px] outline-none " +
+ "focus:border-[var(--primary)] disabled:opacity-70";
if (multiline) {
return (