Implement comprehensive dark/light mode overhaul for admin panel

- Refactor color palette in `ui-design-spec.md` to utilize CSS variables exclusively, eliminating fixed hex values and Tailwind utility classes.
- Complete dark mode implementation in `uiStore.ts`, ensuring proper theme application via `applyTheme()` and `applyBrand()`.
- Create `admin-theme-dark-light-audit.md` to document the transition process, outlining issues with inline styles and fixed colors.
- Introduce `theme-tokens.test.ts` to enforce rules against fixed hex colors and ensure compliance with the design system.
- Update various components and styles to replace inline styles and fixed colors with CSS variables, ensuring consistent theming across light and dark modes.
- Ensure all changes maintain visual integrity in both light and dark modes, with a focus on accessibility and contrast standards.
This commit is contained in:
hamed
2026-07-27 16:41:52 +03:30
parent c29035c21e
commit 55ab2f5dfc
73 changed files with 1485 additions and 1110 deletions
@@ -0,0 +1,324 @@
# اصلاح کامل لایت‌مود / دارک‌مود در پنل ادمین (SPA)
## پروژه
`clinicpro` — فقط فرانت‌اند پنل ادمین (`assets/admin/`). بک‌اند و API دست نمی‌خورد.
## زمینه
پنل ادمین یک design-system کامل مبتنی بر CSS variable دارد که در `assets/admin/styles.css` تعریف شده:
- `:root` مقادیر لایت (`--bg`, `--surface`, `--text`, `--text-2`, `--border`, `--primary`, `--danger`, …)
- `[data-theme="dark"]` همان توکن‌ها را برای دارک بازتعریف می‌کند
- `assets/admin/stores/uiStore.ts``applyTheme()` روی `document.documentElement` هم کلاس `dark` و هم `data-theme` را ست می‌کند، پس **هم `dark:` واریانت Tailwind و هم `var(--token)` هر دو فعال‌اند**
مشکل این است که بخش بزرگی از صفحات این سیستم را دور می‌زنند و رنگ ثابت می‌نویسند. آمار واقعی امروز (اندازه‌گیری‌شده روی `assets/admin`، بدون فایل‌های تست):
| الگو | تعداد |
|---|---|
| `style={{ color/background: ... }}` | ۱۰۳۷ نقطه در ~۶۰ فایل |
| hex ثابت داخل `.tsx` | ۷۵۲ |
| utility رنگ ثابت Tailwind (`bg-white`, `text-slate-400`, `border-gray-100`, …) | ۸۸۵ در ۲۰ فایل |
| از این‌ها با جفت `dark:` | ۳۲۳ |
| کلاس‌های Tailwind با توکن (`text-[var(--text-3)]`) | فقط ۱۵ مورد |
## مشکل / هدف
در دارک‌مود بخش‌هایی از پنل روشن/ناخوانا می‌مانند. سه دستهٔ خرابی، به ترتیب اهمیت:
### دستهٔ A — `style` inline با رنگ ثابت (خرابی قطعی)
inline style قابل override با `dark:` نیست. در ~۹۰ نقطه دقیقاً همین اشتباه رخ داده: کنار inline style یک کلاس `dark:` نوشته شده که **هیچ‌وقت اعمال نمی‌شود** چون specificity کلاس همیشه کمتر از style attribute است.
فایل‌های آلوده به این الگوی مرده:
| فایل | تعداد نقطهٔ `dark:` مرده |
|---|---|
| `components/session/PaymentStep.tsx` | ۲۷ |
| `components/session/DetailsStep.tsx` | ۲۶ |
| `components/session/CreateStep.tsx` | ۱۴ |
| `components/SessionPaymentAccordion.tsx` | ۷ |
| `components/PatientCaseBanner.tsx` | ۷ |
| `components/SessionServiceCard.tsx` | ۵ |
| `pages/SessionPaymentPage.tsx`, `pages/NewSessionPage.tsx`, `pages/EditSessionPage.tsx`, `components/AppointmentTurnCard.tsx` | ۲ هرکدام |
پرمصرف‌ترین فایل‌ها از نظر کل inline style رنگی: `pages/MyPatientsPage.tsx` (۶۷)، `pages/PatientDetailPage.tsx` (۶۲)، `pages/SmsWalletPage.tsx` (۴۶)، `components/session/PaymentStep.tsx` (۴۳)، `pages/SubscriptionPage.tsx` (۳۱)، `pages/DoctorDetailPage.tsx` (۳۰)، `pages/ClinicServicesPage.tsx` (۲۹)، `components/session/DetailsStep.tsx` (۲۸)، `components/session/CreateStep.tsx` (۲۷)، `pages/PatientsListPage.tsx` (۲۶).
### دستهٔ B — utility روشن بدون جفت `dark:`
| فایل | utility رنگی | تعداد `dark:` |
|---|---|---|
| `pages/RepresentationDetailPage.tsx` | ۳۳ | ۱ |
| `pages/AppointmentDetailPage.tsx` | ۲۷ | ۲ |
| `pages/PaymentDetailPage.tsx` | ۱۲ | ۲ |
| `components/ImageCropModal.tsx` | ۲۳ | ۹ (ناقص) |
### دستهٔ C — CSS ثابت در `styles.css` و آیکون‌های SVG
- `.inv-table thead tr { background: #e1e1e1 }` و `.inv-table th { color: #616161 }` و `.inv-table td { border-bottom: 1px solid #DBDBDB }` — هیچ override `.dark` ندارند (فقط `td` border و hover دارند)
- `.inv-tabs`, `.inv-tab`, `.inv-badge.*` با hex ثابت + override دستی `.dark` — به‌جای توکن
- آیکون‌های SVG با `stroke="#616161"` / `stroke="#3B3B3B"` ثابت (مثلاً `pages/MySecretariesPage.tsx` خط ۲۶–۳۵، `components/icons/`) در دارک‌مود روی پس‌زمینهٔ تیره محو می‌شوند
**هدف:** هر رنگ سطحی/متنی/حاشیه‌ای در پنل ادمین از توکن‌های `styles.css` بیاید، طوری که toggle دارک‌مود بدون هیچ نقطهٔ روشن/ناخوانا کار کند — و رگرسیون بعدی با تست گارد گرفته شود.
## معیار پذیرش
-**موفق:** با `darkMode: true` (کلید `clinicpro-ui` در localStorage یا دکمهٔ toggle در topbar)، در هر صفحهٔ فهرست «فایل‌های مرتبط» هیچ کارت/متن/حاشیه/badge با رنگ لایت باقی نمی‌ماند؛ کنتراست متن اصلی روی `--surface` حداقل ۴.۵:۱ و متن ثانویه حداقل ۳:۱.
-**موفق (لایت):** بعد از تغییرات، همان صفحات در لایت‌مود **دقیقاً** مثل قبل رندر می‌شوند — این ریفکتور نباید ظاهر لایت را عوض کند (توکن‌ها همان hexهای فعلی‌اند).
-**خطا / رگرسیون:** یک تست گارد (`assets/admin/test/theme-tokens.test.ts`) سورس را اسکن می‌کند و اگر در فایل‌های پاک‌شده دوباره hex ثابت رنگی یا utility رنگی بدون جفت `dark:` اضافه شود، fail می‌شود.
- ⚠️ **مرزی ۱:** تغییر brand hue (`setBrandHue`، مثلاً هیوی «نارنجی» که `fixed` دارد) در **هر دو** مود باید رنگ اصلی را عوض کند؛ هیچ جایی نباید `#5559CE` ثابت بماند.
- ⚠️ **مرزی ۲:** `density: compact` و مرورگر قدیمی بدون پشتیبانی `color-mix/oklch` (بلوک `@supports` در `styles.css`) — رنگ‌ها باید روی fallback sRGB هم درست باشند، نه اینکه به transparent بیفتند.
- ⚠️ **مرزی ۳:** جدول انبار (`.inv-table`) در دارک‌مود — thead، badgeها (`in-stock`/`low-stock`/`out-of-stock`) و tabها خوانا باشند.
## فایل‌های مرتبط
| فایل | نقش |
|---|---|
| `assets/admin/styles.css` | تعریف توکن‌ها (`:root`, `[data-theme="dark"]`, `@supports`) + کلاس‌های `.inv-*` که هنوز hex ثابت دارند |
| `assets/admin/stores/uiStore.ts` | `applyTheme()` / `applyBrand()` — منبع کلاس `dark` و `data-theme` و `--brand-h/--brand-c` |
| `assets/admin/components/session/PaymentStep.tsx` | بیشترین inline style رنگی + `dark:`های مرده |
| `assets/admin/components/session/DetailsStep.tsx` | همان الگو |
| `assets/admin/components/session/CreateStep.tsx` | همان الگو |
| `assets/admin/components/SessionPaymentAccordion.tsx`, `SessionServiceCard.tsx`, `PatientCaseBanner.tsx`, `AppointmentTurnCard.tsx` | `dark:`های مرده کنار inline style |
| `assets/admin/pages/MyPatientsPage.tsx`, `PatientDetailPage.tsx`, `SmsWalletPage.tsx`, `SubscriptionPage.tsx`, `ClinicServicesPage.tsx`, `PatientsListPage.tsx`, `MySecretariesPage.tsx` | حجم بالای inline style رنگی |
| `assets/admin/pages/RepresentationDetailPage.tsx`, `AppointmentDetailPage.tsx`, `PaymentDetailPage.tsx` | utility روشن بدون `dark:` |
| `assets/admin/components/ImageCropModal.tsx` | جفت `dark:` ناقص |
| `assets/admin/components/icons/` | SVGهای با stroke/fill ثابت |
| `docs/admin-ui/ui-design-spec.md` | سند UI — بخش ۱۶ «Dark Mode (اختیاری — فاز دوم)» کهنه است و با پیاده‌سازی فعلی نمی‌خواند |
## وضعیت فعلی
### الگوی مردهٔ `dark:` کنار inline style — `components/session/PaymentStep.tsx`
```tsx
// خط ۳۳–۳۵ — استایل‌های مشترک، همه hex ثابت
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' };
// خط ۱۵۰ — کلاس dark: هیچ اثری ندارد چون inline style غالب است
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 700, color: '#525252' }}>{formatRial(finalPrice)}</span>
// خط ۱۵۵ — همان اشتباه روی border/background
<div className="dark:border-[#35343D]" style={{ border: '1px solid #E8EBFF', background: '#F7F8FF', borderRadius: 8, padding: 12, marginBottom: 16 }}>
```
### utility روشن بدون جفت dark — `pages/AppointmentDetailPage.tsx`
```tsx
// خط ۱۴۱–۱۴۲
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
<h3 className="font-semibold text-gray-800 mb-4">اطلاعات بیمار</h3>
```
### 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"
// خط ۳۰۹
<p className="text-sm text-gray-400 text-center py-6">اطلاعات بانکی ثبت نشده</p>
```
### 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
<p style={{ fontSize: 12, color: 'var(--text-3)', margin: '0 0 12px', lineHeight: 1.7 }}>
<span style={{ color: 'var(--danger)' }}> *</span>
```
این همان الگویی است که باید همه‌جا اعمال شود: 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
// قبل
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 700, color: '#525252' }}>
// بعد
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>
```
```tsx
// قبل
<div className="dark:border-[#35343D]" style={{ border: '1px solid #E8EBFF', background: '#F7F8FF', ... }}>
// بعد
<div style={{ border: '1px solid var(--border)', background: 'var(--primary-soft)', ... }}>
```
ثابت‌های ماژول‌سطح (`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 رنگ می‌گیرد (مثل `<PatientsGridView color={...} />`) — در وظیفهٔ ۶
- **هر فایل یک 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
// قبل
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
<h3 className="font-semibold text-gray-800 mb-4">اطلاعات بیمار</h3>
// بعد
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6">
<h3 className="font-semibold text-[var(--text)] mb-4">اطلاعات بیمار</h3>
```
```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
// قبل
<path d="…" stroke="#616161" strokeWidth="1.5" />
// بعد — رنگ از والد
<span className="text-[var(--text-2)]">
<path d="…" stroke="currentColor" strokeWidth="1.5" />
</span>
```
**نحوه تست:** صفحهٔ «منشی‌های من» و «سرویس‌ها» در دارک‌مود — آیکون‌ها باید دیده شوند نه اینکه در پس‌زمینه گم شوند.
### ۷. تست گارد + به‌روزرسانی سند
**۷.۱ تست گارد**`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` روی `<html>` توسط `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`.
@@ -26,13 +26,13 @@ function InfoRow({ icon, label, value, chip = false, valueStyle }: {
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{icon}
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 12, color: '#616161' }}>{label}:</span>
<span style={{ fontSize: 12, color: 'var(--text-2)' }}>{label}:</span>
</div>
<span
className={chip ? 'dark:bg-[#404040] dark:text-[#D7D8ED]' : 'dark:text-[#D7D8ED]'}
className={chip ? 'dark:bg-[var(--surface-2)]' : ''}
style={{
fontSize: 12, color: '#525252', textAlign: 'left',
...(chip ? { background: '#efefef', borderRadius: 8, padding: '2px 8px', color: '#2f2f2f' } : {}),
fontSize: 12, color: 'var(--text)', textAlign: 'left',
...(chip ? { background: 'var(--surface-2)', borderRadius: 8, padding: '2px 8px', color: 'var(--text)' } : {}),
...valueStyle,
}}
>
@@ -55,7 +55,7 @@ export default function AppointmentTurnCard({ appointment, queryKey }: {
const title = appointment.service_name || 'نوبت';
return (
<div
className="bg-white dark:bg-[#222433] border border-[#EDEDED] dark:border-[#35343D]"
className="bg-[var(--surface)] border border-[var(--border)]"
style={{ width: 277, maxWidth: '100%', minHeight: 249, borderRadius: 12, padding: 14, display: 'flex', flexDirection: 'column', gap: 10, boxShadow: '0 1px 6px rgba(15,23,42,0.06)' }}
>
{/* Header */}
@@ -63,13 +63,13 @@ export default function AppointmentTurnCard({ appointment, queryKey }: {
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
<FilesServiceSuccess />
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 3, minWidth: 0 }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 600, color: '#525252', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 180 }}>{title}</span>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 180 }}>{title}</span>
</div>
</div>
<FilesServiceMore style={{ color: '#9CA3AF', flexShrink: 0 }} />
<FilesServiceMore style={{ color: 'var(--text-3)', flexShrink: 0 }} />
</div>
<div className="dark:border-[#35343D]" style={{ borderTop: '1px solid #F1F1F1' }} />
<div style={{ borderTop: '1px solid var(--border)' }} />
{/* Middle: date & time chips */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
@@ -77,7 +77,7 @@ export default function AppointmentTurnCard({ appointment, queryKey }: {
<InfoRow icon={<ClockP />} label="ساعت" chip value={formatTime(appointment.starts_at)} />
</div>
<div className="dark:border-[#35343D]" style={{ borderTop: '1px solid #F1F1F1' }} />
<div style={{ borderTop: '1px solid var(--border)' }} />
{/* Bottom: personnel & status */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
+2 -2
View File
@@ -79,7 +79,7 @@ export default function FreeVisitPrice({ doctorUuid, readOnly = false }: { docto
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 10, fontSize: 13, cursor: 'pointer', marginTop: 16 }}>
<span style={{
position: 'relative', width: 42, height: 22, borderRadius: 999, flexShrink: 0,
background: required ? 'var(--primary)' : '#c4c4c4', transition: 'background .2s',
background: required ? 'var(--primary)' : 'var(--border-2)', transition: 'background .2s',
}}>
<input
type="checkbox" checked={required} role="switch" aria-label="الزامی کردن هزینه ویزیت"
@@ -88,7 +88,7 @@ export default function FreeVisitPrice({ doctorUuid, readOnly = false }: { docto
/>
<span style={{
position: 'absolute', top: 2, insetInlineStart: required ? 22 : 2, width: 18, height: 18,
borderRadius: 999, background: '#fff', transition: 'inset-inline-start .2s', boxShadow: '0 1px 2px rgba(0,0,0,.2)',
borderRadius: 999, background: 'var(--surface)', transition: 'inset-inline-start .2s', boxShadow: '0 1px 2px rgba(0,0,0,.2)',
}} />
</span>
الزامی کردن هزینه ویزیت
+8 -8
View File
@@ -31,14 +31,14 @@ export default function ImageCropModal({ src, fileName, onCancel, onConfirm }: I
return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" dir="rtl">
<div className="w-full max-w-md rounded-2xl bg-white dark:bg-gray-900 shadow-2xl overflow-hidden">
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-100 dark:border-gray-800">
<p className="text-base font-bold text-slate-800 dark:text-slate-100">برش تصویر</p>
<div className="w-full max-w-md rounded-2xl bg-[var(--surface)] shadow-2xl overflow-hidden">
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--border)]">
<p className="text-base font-bold text-[var(--text)]">برش تصویر</p>
<button onClick={onCancel}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 text-xl leading-none">×</button>
className="text-[var(--text-3)] hover:text-[var(--text-2)] text-xl leading-none">×</button>
</div>
<div className="relative w-full h-72 bg-slate-100 dark:bg-gray-800">
<div className="relative w-full h-72 bg-[var(--surface-2)]">
<Cropper
image={src}
crop={crop}
@@ -54,18 +54,18 @@ export default function ImageCropModal({ src, fileName, onCancel, onConfirm }: I
<div className="px-5 py-4 space-y-4">
<div className="flex items-center gap-3">
<span className="text-xs text-slate-500 dark:text-slate-400 shrink-0">بزرگنمایی</span>
<span className="text-xs text-[var(--text-2)] shrink-0">بزرگنمایی</span>
<input type="range" min={1} max={3} step={0.01} value={zoom}
onChange={e => setZoom(Number(e.target.value))}
className="w-full accent-indigo-600" />
</div>
<div className="flex items-center justify-end gap-3">
<button onClick={onCancel} disabled={processing}
className="px-4 py-2 rounded-xl text-sm font-medium text-slate-600 dark:text-slate-300 bg-slate-100 dark:bg-gray-800 hover:bg-slate-200 dark:hover:bg-gray-700 disabled:opacity-50">
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">
انصراف
</button>
<button onClick={handleConfirm} disabled={processing || !area}
className="px-4 py-2 rounded-xl text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 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 ? 'در حال برش...' : 'ذخیره'}
</button>
</div>
@@ -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) : '—',
<span style={{ color: remaining > 0 ? '#d32f2f' : '#388e3c', fontWeight: 600 }}>{statusLabel}</span>,
<span style={{ color: remaining > 0 ? 'var(--danger)' : 'var(--success)', fontWeight: 600 }}>{statusLabel}</span>,
]]}
/>
{(inv.base_insurance_name || inv.service_category_label) && (
@@ -168,7 +168,7 @@ export default function InvoiceSummaryModal({ invoiceUuid, onClose }: { invoiceU
cols={['مبلغ کل پرداخت شده', 'مبلغ باقی مانده']}
rows={[[
formatRial(paidAmount),
<span style={{ color: remaining > 0 ? '#d32f2f' : '#388e3c', fontWeight: 600 }}>{formatRial(remaining)}</span>,
<span style={{ color: remaining > 0 ? 'var(--danger)' : 'var(--success)', fontWeight: 600 }}>{formatRial(remaining)}</span>,
]]}
/>
</div>
@@ -309,7 +309,7 @@ export default function NewAppointmentDrawer({ doctorUuid, defaultDate, queryKey
<button key={s.start} type="button" dir="ltr" onClick={() => 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}
</button>
);
+11 -11
View File
@@ -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 (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#6B7280', fontSize: 12, marginBottom: 14 }}>
<Link to={backTo} className="bg-white dark:bg-[#222433]" style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer', color: '#6B7280', textDecoration: 'none' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-2)', fontSize: 12, marginBottom: 14 }}>
<Link to={backTo} className="bg-[var(--surface)]" style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer', color: 'var(--text-2)', textDecoration: 'none' }}>
<ArrowLeftPH />
<span>بازگشت</span>
</Link>
<ArrowLeftD />
<span>پرونده</span>
<ArrowLeftD />
<span className="dark:text-[#D7D8ED]" style={{ color: '#111827' }}>{name}</span>
<span style={{ color: 'var(--text)' }}>{name}</span>
</div>
);
}
@@ -37,8 +37,8 @@ function TagDots({ tags }: { tags?: Tag[] }) {
const InfoLine = ({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{icon}
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 12, color: '#2f2f2f' }}>{label}</span>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 500, color: '#2f2f2f' }} dir="ltr">{value}</span>
<span style={{ fontSize: 12, color: 'var(--text)' }}>{label}</span>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }} dir="ltr">{value}</span>
</div>
);
@@ -60,21 +60,21 @@ export default function PatientCaseBanner({ name, recordNumber, mobile, createdA
const complete = !hasDebt;
return (
<div
className="bg-white dark:bg-[#222433] border border-[#EDEDED] dark:border-[#35343D]"
className="bg-[var(--surface)] border border-[var(--border)]"
style={{ borderRadius: 16, padding: '14px 20px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24, marginBottom: 16, minHeight: 140, flexWrap: 'wrap' }}
>
{/* right — name + tags */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
<span className="dark:text-[#D7D8ED]" style={{ fontWeight: 700, fontSize: 20, color: '#111827' }}>{name}</span>
<span style={{ fontWeight: 700, fontSize: 20, color: 'var(--text)' }}>{name}</span>
<span style={{
height: 29, minWidth: 95, borderRadius: 8, padding: '0 10px', fontSize: 13, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
background: complete ? '#ECFDF3' : 'rgba(255,192,81,0.15)', color: complete ? '#3C9A4F' : '#f59e0b',
background: complete ? 'var(--success-bg)' : 'rgba(255,192,81,0.15)', color: complete ? 'var(--success)' : 'var(--warning)',
}}>{complete ? 'تکمیل شده' : 'تکمیل نشده'}</span>
</div>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 16, color: '#525252' }}>شماره پرونده: {recordNumber || '—'}</span>
<span style={{ fontSize: 16, color: 'var(--text)' }}>شماره پرونده: {recordNumber || '—'}</span>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 16 }}>
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 16, color: '#6B7280' }}>برچسب ها:</span>
<span style={{ fontSize: 16, color: 'var(--text-2)' }}>برچسب ها:</span>
<TagDots tags={tags} />
</div>
</div>
@@ -90,7 +90,7 @@ export default function PatientCaseBanner({ name, recordNumber, mobile, createdA
<InfoLine icon={<FilesServiceNotification />} label="نوبت بعدی:" value={nextAppointment ? formatDate(nextAppointment) : '—'} />
<button
type="button" onClick={onAddNote}
style={{ display: 'inline-flex', alignItems: 'center', gap: 6, background: '#f17732', color: '#fff', border: 'none', borderRadius: 10, padding: '0 18px', height: 36, fontSize: 13, fontWeight: 600, cursor: 'pointer' }}
style={{ display: 'inline-flex', alignItems: 'center', gap: 6, background: 'var(--accent)', color: 'var(--on-primary)', border: 'none', borderRadius: 10, padding: '0 18px', height: 36, fontSize: 13, fontWeight: 600, cursor: 'pointer' }}
>
<FilesServiceMessage /> یادداشت
</button>
+1 -1
View File
@@ -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 ? <XMarkIcon style={{ width: 12 }} /> : <PlusIcon style={{ width: 12 }} />}
@@ -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}
@@ -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 <span className="dark:bg-[#35343D]" style={{ width: 1, height: h, background: '#E5E7EB', flexShrink: 0, margin: '0 16px', alignSelf: 'center' }} />;
return <span style={{ width: 1, height: h, background: 'var(--surface-2)', flexShrink: 0, margin: '0 16px', alignSelf: 'center' }} />;
}
function Meta({ label, value }: { label: string; value: string }) {
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13 }}>
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>{label}:</span>
<span className="dark:text-[#D7D8ED]" style={{ color: '#525252', fontWeight: 500 }}>{value}</span>
<span style={{ color: 'var(--text-2)' }}>{label}:</span>
<span style={{ color: 'var(--text)', fontWeight: 500 }}>{value}</span>
</span>
);
}
@@ -49,16 +49,16 @@ export default function SessionPaymentAccordion({ session, expanded, onToggle }:
const finalPrice = formatRial(session.final_price_rials ?? 0);
return (
<div className="dark:border-[#35343D]" style={{ border: '1px solid #E5E7EB', borderRadius: 8, marginBottom: 16, overflow: 'hidden' }}>
<div style={{ border: '1px solid var(--border)', borderRadius: 8, marginBottom: 16, overflow: 'hidden' }}>
{/* summary */}
<button
type="button"
onClick={onToggle}
aria-expanded={expanded}
className="bg-white dark:bg-[#2B2D3A]"
className="bg-[var(--surface)]"
style={{ display: 'flex', alignItems: 'center', gap: 4, width: '100%', padding: '12px 16px', border: 'none', cursor: 'pointer', textAlign: 'start', flexWrap: 'wrap' }}
>
<span className="dark:text-[#D7D8ED]" style={{ minWidth: 150, color: '#2f2f2f', fontWeight: 500, fontSize: 14 }}>{name}</span>
<span style={{ minWidth: 150, color: 'var(--text)', fontWeight: 500, fontSize: 14 }}>{name}</span>
<VDivider />
<Meta label="تاریخ" value={session.created_at ? formatDate(session.created_at) : '—'} />
<VDivider />
@@ -66,26 +66,26 @@ export default function SessionPaymentAccordion({ session, expanded, onToggle }:
<span style={{ flex: 1 }} />
<VDivider />
<span style={{
color: '#fff', padding: '3px 16px', borderRadius: 4, fontSize: 12, fontWeight: 600,
textAlign: 'center', minWidth: 90, background: paid ? '#10B981' : '#F59E0B',
color: 'var(--on-primary)', padding: '3px 16px', borderRadius: 4, fontSize: 12, fontWeight: 600,
textAlign: 'center', minWidth: 90, background: paid ? 'var(--success)' : 'var(--warning)',
}}>
{paid ? 'پرداخت شده' : 'تسویه نشده'}
</span>
<ChevronDownIcon className="dark:text-[#D7D8ED]" style={{ width: 20, height: 20, color: '#525252', marginInlineStart: 8, transition: 'transform .2s', transform: expanded ? 'rotate(180deg)' : 'none' }} />
<ChevronDownIcon style={{ width: 20, height: 20, color: 'var(--text)', marginInlineStart: 8, transition: 'transform .2s', transform: expanded ? 'rotate(180deg)' : 'none' }} />
</button>
{/* details */}
{expanded && (
<div className="dark:border-[#35343D] dark:bg-[#222433]" style={{ borderTop: '1px solid #E5E7EB' }}>
<div className="dark:bg-[var(--surface)]" style={{ borderTop: '1px solid var(--border)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '12px 16px 8px' }}>
<FilesServicePaymentsCheck color="#6B7280" size={18} style={{ transform: 'rotate(180deg)' }} />
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280', fontWeight: 600, fontSize: 13 }}>پرداختیها</span>
<FilesServicePaymentsCheck color="var(--text-2)" size={18} style={{ transform: 'rotate(180deg)' }} />
<span style={{ color: 'var(--text-2)', fontWeight: 600, fontSize: 13 }}>پرداختیها</span>
</div>
<div style={{ padding: '0 16px 16px' }}>
{paid ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '12px 0', flexWrap: 'wrap' }}>
<FilesServicePaymentsCheck color="#6B7280" size={20} />
<FilesServicePaymentsCheck color="var(--text-2)" size={20} />
<Meta label="تاریخ" value={formatDate(session.updated_at ?? session.created_at ?? 0)} />
<VDivider h={32} />
<Meta label="مبلغ" value={finalPrice} />
@@ -95,7 +95,7 @@ export default function SessionPaymentAccordion({ session, expanded, onToggle }:
<Meta label="پرسنل" value={session.doctor_name || '—'} />
</div>
) : (
<div className="dark:text-[#A1A1A1]" style={{ padding: 16, textAlign: 'center', color: '#6B7280', fontSize: 13 }}>هیچ پرداختی ثبت نشده است.</div>
<div style={{ padding: 16, textAlign: 'center', color: 'var(--text-2)', fontSize: 13 }}>هیچ پرداختی ثبت نشده است.</div>
)}
</div>
</div>
+15 -15
View File
@@ -55,8 +55,8 @@ export interface SessionCardData {
function Row({ label, value, valueStyle }: { label: string; value: string; valueStyle?: CSSProperties }) {
return (
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 6, margin: '6px 0' }}>
<span style={{ fontSize: 12, color: '#616161' }} className="dark:text-[#A1A1A1]">{label}:</span>
<span style={{ fontSize: 14, color: '#525252', textAlign: 'left', ...valueStyle }} className="dark:text-[#D7D8ED]">{value}</span>
<span style={{ fontSize: 12, color: 'var(--text-2)' }}>{label}:</span>
<span style={{ fontSize: 14, color: 'var(--text)', textAlign: 'left', ...valueStyle }}>{value}</span>
</div>
);
}
@@ -99,27 +99,27 @@ export default function SessionServiceCard({ session, onSettle, onViewInvoice, o
return (
<div
className="bg-white dark:bg-[#222433] border border-[#EDEDED] dark:border-[#35343D]"
className="bg-[var(--surface)] border border-[var(--border)]"
style={{ minWidth: 277, minHeight: 331, borderRadius: 12, padding: 14, display: 'flex', flexDirection: 'column', gap: 10, overflow: 'hidden', boxShadow: '0 1px 6px rgba(15,23,42,0.06)' }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 2 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
<FilesServiceSuccess />
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 3, minWidth: 0 }}>
<span className="dark:text-[#D7D8ED]" style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, fontWeight: 600, color: '#525252', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 180 }}>
<span style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 14, fontWeight: 600, color: 'var(--text)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 180 }}>
{title}
{session.archived && <span className="badge" style={{ fontSize: 10, color: '#9CA3AF', border: '1px solid #E0E0E0', borderRadius: 4, padding: '1px 6px' }}>آرشیو</span>}
{session.archived && <span className="badge" style={{ fontSize: 10, color: 'var(--text-3)', border: '1px solid var(--border-2)', borderRadius: 4, padding: '1px 6px' }}>آرشیو</span>}
</span>
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 12, color: '#6B7280', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 200 }}>{subtitle}</span>
<span style={{ fontSize: 12, color: 'var(--text-2)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 200 }}>{subtitle}</span>
</div>
</div>
<div ref={menuRef} style={{ position: 'relative', flexShrink: 0 }}>
<button type="button" aria-label="عملیات" onClick={() => setMenuOpen((o) => !o)}
style={{ background: 'transparent', border: 'none', cursor: 'pointer', padding: 2, display: 'flex' }}>
<FilesServiceMore style={{ color: '#9CA3AF' }} />
<FilesServiceMore style={{ color: 'var(--text-3)' }} />
</button>
{menuOpen && (
<div className="card dark:bg-[#222433] dark:border-[#35343D]" style={{ position: 'absolute', insetInlineEnd: 0, top: '100%', marginTop: 4, minWidth: 150, zIndex: 20, padding: 4, background: '#fff', border: '1px solid var(--border)', borderRadius: 8, boxShadow: 'var(--shadow-lg)' }}>
<div className="card" style={{ position: 'absolute', insetInlineEnd: 0, top: '100%', marginTop: 4, minWidth: 150, zIndex: 20, padding: 4, background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 8, boxShadow: 'var(--shadow-lg)' }}>
<button type="button" onClick={() => { 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 && (
<button type="button" onClick={() => { 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 ? 'خروج از آرشیو' : 'آرشیو'}
</button>
)}
@@ -147,7 +147,7 @@ export default function SessionServiceCard({ session, onSettle, onViewInvoice, o
</div>
</div>
<div className="dark:border-[#35343D]" style={{ borderTop: '1px solid #F1F1F1' }} />
<div style={{ borderTop: '1px solid var(--border)' }} />
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, overflow: 'hidden', lineHeight: 1.9 }}>
<Row label="انجام دهنده" value={session.doctor_name || '—'} />
@@ -155,7 +155,7 @@ export default function SessionServiceCard({ session, onSettle, onViewInvoice, o
<Row label="توضیحات" value={session.notes || '—'} valueStyle={{ display: 'flex', textAlign: 'right', width: '100%' }} />
</div>
<div className="dark:border-[#35343D]" style={{ borderTop: '1px solid #F1F1F1' }} />
<div style={{ borderTop: '1px solid var(--border)' }} />
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
{session.insurance_service_category_label && (
@@ -163,10 +163,10 @@ export default function SessionServiceCard({ session, onSettle, onViewInvoice, o
)}
{session.insurance_base_name && <Row label="بیمه" value={session.insurance_base_name} />}
{(session.base_insurance_rials ?? 0) > 0 && (
<Row label="سهم بیمه" value={formatRial(session.base_insurance_rials ?? 0)} valueStyle={{ color: '#16A34A', fontWeight: 500 }} />
<Row label="سهم بیمه" value={formatRial(session.base_insurance_rials ?? 0)} valueStyle={{ color: 'var(--success)', fontWeight: 500 }} />
)}
<Row label="هزینه" value={formatRial(session.final_price_rials ?? 0)} valueStyle={{ fontWeight: 500 }} />
{!paid && <Row label="مانده بدهی" value={formatRial(debt)} valueStyle={{ color: '#EF4444', fontWeight: 500 }} />}
{!paid && <Row label="مانده بدهی" value={formatRial(debt)} valueStyle={{ color: 'var(--danger)', fontWeight: 500 }} />}
</div>
<div style={{ marginTop: 'auto' }}>
@@ -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 ? 'در حال صدور…' : 'مشاهده فاکتور'}
</button>
@@ -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)' }}
>
تکمیل پرداخت
</button>
@@ -23,9 +23,9 @@ describe('SessionStepper', () => {
it('کانکتور گام‌های طی‌شده بنفش و بقیه خاکستری', () => {
render(<SessionStepper steps={STEPS} activeStep={1} />);
// 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('استپر خالی: بدون کرش و بدون کانکتور', () => {
+4 -4
View File
@@ -21,8 +21,8 @@ function StepIcon({ index, active, completed }: { index: number; active: boolean
if (completed && !active) {
// دایره سفید با تیک سبز (tauri ColorlibStepIconRoot completed state)
return (
<div style={{ width: 52, height: 52, borderRadius: '50%', background: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg width="32" height="32" viewBox="0 0 24 24" fill="#22C55E" xmlns="http://www.w3.org/2000/svg">
<div style={{ width: 52, height: 52, borderRadius: '50%', background: 'var(--surface)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg width="32" height="32" viewBox="0 0 24 24" style={{ fill: 'var(--success)' }} xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
</svg>
</div>
@@ -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)',
}}
/>
)}
<StepIcon index={ICON_BY_LABEL[label] ?? i + 1} active={i === activeStep} completed={i < activeStep} />
<span className="dark:text-[#D7D8ED]" style={{ marginTop: 8, fontSize: '0.875rem', fontWeight: 600, color: '#3b3b3b' }}>
<span style={{ marginTop: 8, fontSize: '0.875rem', fontWeight: 600, color: 'var(--text)' }}>
{label}
</span>
</div>
@@ -382,7 +382,7 @@ function StatusToggle({ contract, onToggle, disabled }: { contract: Contract; on
background: contract.is_active ? 'var(--primary)' : 'var(--border)',
}}>
<span style={{
position: 'absolute', top: 2, width: 16, height: 16, borderRadius: 999, background: '#fff', transition: 'inset-inline .2s',
position: 'absolute', top: 2, width: 16, height: 16, borderRadius: 999, background: 'var(--surface)', transition: 'inset-inline .2s',
insetInlineStart: contract.is_active ? 18 : 2,
}} />
</span>
@@ -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 && <span style={{ width: 8, height: 8, background: '#fff', borderRadius: 2 }} />}
{active && <span style={{ width: 8, height: 8, background: 'var(--surface)', borderRadius: 2 }} />}
</span>
{s.name}
</span>
@@ -182,7 +182,7 @@ export default function ServiceSlotPicker({
<button type="button" aria-label={`حذف ${s.name}`} onClick={() => 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',
}}>×</button>
</div>
@@ -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}
</button>
@@ -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 (
@@ -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,
}}
>
<InnerTimes start={slot.start_time} end={slot.end_time} color={isPast ? '#9E9E9E' : cfg.dotColor} />
<InnerTimes start={slot.start_time} end={slot.end_time} color={isPast ? 'var(--text-3)' : cfg.dotColor} />
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
<span style={{ fontSize: 13.5, color: isPast ? 'var(--text-3)' : cfg.textColor, fontWeight: 500 }}>
{isPast ? 'گذشته' : 'افزودن نوبت سریع'}
@@ -88,8 +88,8 @@ function EmptyCard({ slot, onBook }: { slot: TimelineSlot; onBook: (s: TimelineS
{!isPast && (
<span style={{
display: 'inline-flex', alignItems: 'center', gap: 5, padding: '3px 10px', borderRadius: 99,
fontSize: 12, fontWeight: 700, color: cfg.textColor, background: `${cfg.dotColor}15`,
border: `1.5px solid ${cfg.dotColor}30`, whiteSpace: 'nowrap', alignSelf: 'flex-start',
fontSize: 12, fontWeight: 700, color: cfg.textColor, background: `color-mix(in srgb, ${cfg.dotColor} 8%, transparent)`,
border: `1.5px solid color-mix(in srgb, ${cfg.dotColor} 19%, transparent)`, whiteSpace: 'nowrap', alignSelf: 'flex-start',
}}>
<span style={{ width: 7, height: 7, borderRadius: '50%', background: cfg.dotColor }} />
نوبت جدید
@@ -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<TurnStatusConfig, 'label'> => ({
bgColor: `var(--${bg})`, borderColor: `var(--${fg})`,
dotColor: `var(--${fg})`, textColor: `var(--${fg})`,
});
// وضعیت‌های بک‌اند → توکن‌های تم (هیوی طرح tauri حفظ شده است).
const STATUS: Record<string, TurnStatusConfig> = {
// 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 {
@@ -56,20 +56,20 @@ export function NewAppointmentsTable({ rows, loading, queryKey, emptyText }: Pro
}
if (!rows.length) {
return (
<p className="text-center py-[32px] text-[13.5px] text-[#7E7E7E] dark:text-[#A1A1A1]">
<p className="text-center py-[32px] text-[13.5px] text-[var(--text-2)]">
{emptyText ?? 'نوبتی برای امروز ثبت نشده'}
</p>
);
}
return (
<div className="w-full overflow-x-auto rounded-[8px] border border-solid border-[#E7E7E7] dark:border-[#35343D]">
<div className="w-full overflow-x-auto rounded-[8px] border border-solid border-[var(--border)]">
<table className="w-full border-collapse">
<thead>
<tr className="bg-[#EFEFEF] dark:bg-[#35343D]">
<tr className="bg-[var(--surface-2)]">
{HEAD.map((h, i) => (
<th
key={h}
className={`text-[#616161] dark:text-[#D7D8ED] text-[14px] font-normal py-[10px] px-[18px] whitespace-nowrap ${i === HEAD.length - 1 ? 'text-center' : 'text-start'}`}
className={`text-[var(--text-2)] dark:text-[var(--text)] text-[14px] font-normal py-[10px] px-[18px] whitespace-nowrap ${i === HEAD.length - 1 ? 'text-center' : 'text-start'}`}
>
{h}
</th>
@@ -80,7 +80,7 @@ export function NewAppointmentsTable({ rows, loading, queryKey, emptyText }: Pro
{rows.map((r, idx) => (
<tr
key={r.uuid}
className="border-b border-[#DBDBDB] dark:border-transparent hover:bg-[#f4f5fd] dark:hover:bg-[#2a2c3d] transition-colors"
className="border-b border-[var(--border-2)] dark:border-transparent hover:bg-[var(--primary-soft)] dark:hover:bg-[var(--surface-2)] transition-colors"
>
<Cell>{new Intl.NumberFormat('fa-IR').format(idx + 1)}</Cell>
<Cell>{r.patient_name || '—'}</Cell>
@@ -97,7 +97,7 @@ export function NewAppointmentsTable({ rows, loading, queryKey, emptyText }: Pro
<td className="py-[10px] px-[18px] text-center">
<Link
to={isoDay(r.slot_start) ? `/admin/appointments?date=${isoDay(r.slot_start)}` : '/admin/appointments'}
className="text-[#5559ce] text-[12px] font-medium hover:underline whitespace-nowrap"
className="text-[var(--primary)] text-[12px] font-medium hover:underline whitespace-nowrap"
>
مشاهده
</Link>
@@ -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 (
<span
className="inline-flex items-center gap-[5px] px-[10px] py-[3px] rounded-full text-[12px] font-bold whitespace-nowrap"
@@ -128,7 +128,7 @@ function StatusPill({ status }: { status: string }) {
function Cell({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<td
className={`text-[#616161] dark:text-[#A1A1A1] text-[16px] font-medium py-[10px] px-[18px] whitespace-nowrap text-start ${className}`}
className={`text-[var(--text-2)] text-[16px] font-medium py-[10px] px-[18px] whitespace-nowrap text-start ${className}`}
>
{children}
</td>
@@ -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(<TauriBarChart data={month} />);
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(<TauriBarChart data={month} />);
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');
@@ -91,7 +91,7 @@ function ChartFrame({
<div className="flex-1 flex min-h-0" style={{ alignItems: 'stretch' }}>
{/* y-axis ticks, aligned to gridlines */}
<div
className="flex flex-col justify-between text-[14px] font-normal text-[#858D9D] text-left pl-[4px] shrink-0"
className="flex flex-col justify-between text-[14px] font-normal text-[var(--text-2)] text-left pl-[4px] shrink-0"
style={{ width: yWidth, alignItems: 'flex-start' }}
>
{ticks.map((t, i) => (
@@ -106,7 +106,7 @@ function ChartFrame({
{ticks.map((_, i) => (
<div
key={i}
className="absolute left-0 right-0 border-t border-dashed border-[#E7E7E7] dark:border-[#35343D]"
className="absolute left-0 right-0 border-t border-dashed border-[var(--border)]"
style={{ top: `${(i / (ticks.length - 1)) * 100}%` }}
/>
))}
@@ -116,7 +116,7 @@ function ChartFrame({
{/* x-axis labels */}
<div className="flex pt-[8px]" style={{ paddingRight: yWidth, alignItems: 'flex-start' }}>
{labels.map((l, i) => (
<span key={i} className="flex-1 min-w-0 overflow-hidden text-center text-[10px] font-normal text-[#7E7E7E] whitespace-nowrap">
<span key={i} className="flex-1 min-w-0 overflow-hidden text-center text-[10px] font-normal text-[var(--text-2)] whitespace-nowrap">
{l}
</span>
))}
@@ -145,7 +145,7 @@ export function TauriBarChart({ data }: { data: ChartPoint[] }) {
<div key={i} className="flex-1 min-w-0 flex items-end justify-center" style={{ alignItems: 'flex-end' }}>
<div
title={faNum.format(d.value)}
className="w-[60%] max-w-[24px] rounded-t-[3px] bg-[#5559CE]"
className="w-[60%] max-w-[24px] rounded-t-[3px] bg-[var(--primary)]"
style={{
height: `${(d.value / top) * 100}%`,
minHeight: d.value > 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 (
<div className="h-[300px] w-full grid place-items-center text-[13px] text-[#7E7E7E]">
<div className="h-[300px] w-full grid place-items-center text-[13px] text-[var(--text-2)]">
دادهای برای نمایش نیست
</div>
);
@@ -31,7 +31,7 @@ function SmSelector({ options, value, onChange }: { options: SelectorOption[]; v
}
function ChartTitle({ text }: { text: string }) {
return <p className="text-[#525252] dark:text-[#D7D8ED] text-[14px] md:text-[16px] font-bold">{text}</p>;
return <p className="text-[var(--text)] text-[14px] md:text-[16px] font-bold">{text}</p>;
}
function ChartCard({
@@ -48,7 +48,7 @@ function ChartCard({
children: React.ReactNode;
}) {
return (
<div className="w-full rounded-[8px] border border-solid border-[#EFEFEF] dark:border-transparent bg-[#FFF] dark:bg-[#222433]">
<div className="w-full rounded-[8px] border border-solid border-[var(--border)] dark:border-transparent bg-[var(--surface)]">
<div className="flex px-[20px] py-[12px] md:py-[14px] lg:py-[16px] items-center justify-between gap-[12px]">
<ChartTitle text={title} />
<SmSelector options={selectorOptions} value={selectorValue} onChange={onSelectorChange} />
@@ -141,12 +141,12 @@ export function TauriDashboardView({
</div>
{/* New appointments list */}
<div className="w-full rounded-[8px] border border-solid border-transparent md:border-[#EFEFEF] dark:border-transparent bg-transparent md:bg-[#FFF] md:dark:bg-[#222433]">
<div className="w-full rounded-[8px] border border-solid border-transparent md:border-[var(--border)] dark:border-transparent bg-transparent md:bg-[var(--surface)] md:dark:bg-[var(--surface)]">
<div className="md:pt-[18px] md:px-[24px] md:pb-[20px] flex items-center justify-between">
<p className="text-[#525252] dark:text-[#D7D8ED] text-[14px] md:text-[16px] font-bold">لیست نوبتهای جدید</p>
<p className="text-[var(--text)] text-[14px] md:text-[16px] font-bold">لیست نوبتهای جدید</p>
<Link
to="/admin/appointments"
className="flex items-center gap-[4px] p-1 text-[12px] md:text-[14px] text-[#616161] dark:text-[#A1A1A1] font-normal hover:text-[#5559ce]"
className="flex items-center gap-[4px] p-1 text-[12px] md:text-[14px] text-[var(--text-2)] font-normal hover:text-[var(--primary)]"
>
نوبتها
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
@@ -19,7 +19,7 @@ export interface StatCardModel {
function TauriStatCard({ data }: { data: StatCardModel }) {
return (
<li
className={`px-[8px] md:px-[16px] lg:px-[24px] py-[18px] sm:py-[20px] md:py-[22px] lg:py-[24px] rounded-[8px] flex items-center justify-start gap-[4px] md:gap-[8px] lg:gap-[12px] ${data.cardColor} dark:bg-[#222433]`}
className={`px-[8px] md:px-[16px] lg:px-[24px] py-[18px] sm:py-[20px] md:py-[22px] lg:py-[24px] rounded-[8px] flex items-center justify-start gap-[4px] md:gap-[8px] lg:gap-[12px] ${data.cardColor} dark:bg-[var(--surface)]`}
>
<div
className={`p-[8px] w-[calc(18px+8px)] sm:w-[calc(20px+8px)] md:w-[calc(22px+8px)] lg:w-[calc(24px+8px)] min-w-[calc(18px+8px)] sm:min-w-[calc(20px+8px)] md:min-w-[calc(22px+8px)] lg:min-w-[calc(24px+8px)] h-[calc(18px+8px)] sm:h-[calc(20px+8px)] md:h-[calc(22px+8px)] lg:h-[calc(24px+8px)] grid place-items-center rounded-full ${data.iconColor}`}
@@ -27,10 +27,10 @@ function TauriStatCard({ data }: { data: StatCardModel }) {
{data.icon}
</div>
<div className="flex flex-col w-full items-start justify-center gap-[8px]">
<p className="text-[#3B3B3B] text-wrap dark:text-[#D7D8ED] text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-medium">
<p className="text-[var(--text)] text-wrap text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-medium">
{data.value}
</p>
<p className="text-[#616161] dark:text-[#A1A1A1] text-[12px] md:text-[14px] lg:text-[16px] font-normal">
<p className="text-[var(--text-2)] text-[12px] md:text-[14px] lg:text-[16px] font-normal">
{data.label}
</p>
</div>
@@ -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: <UserAddIcon />,
value: `${formatNumber(stats.totalPatients)}+`,
label: 'تعداد کل مراجعین',
},
{
cardColor: 'bg-[rgba(0,157,121,0.10)]',
iconColor: 'bg-[#009D79]',
iconColor: 'bg-[var(--success)]',
icon: <CardIcon />,
value: formatRial(stats.totalPaymentsRials),
label: 'کل پرداختی‌ها',
},
{
cardColor: 'bg-[rgba(85,89,206,0.08)]',
iconColor: 'bg-[#5559CE]',
iconColor: 'bg-[var(--primary)]',
icon: <CardTickIcon />,
value: formatRial(stats.todayPaymentsRials),
label: 'پرداختی‌های امروز',
},
{
cardColor: 'bg-[rgba(255,192,81,0.11)]',
iconColor: 'bg-[#FFC051]',
iconColor: 'bg-[var(--warning)]',
icon: <CalendarIcon />,
value: `${formatNumber(stats.todayAppointments)}+`,
label: 'تعداد نوبت‌های امروز',
@@ -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 (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M16.6004 7.4585L11.1671 12.8918C10.5254 13.5335 9.47539 13.5335 8.83372 12.8918L3.40039 7.4585" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" style={{ color }}>
<path d="M16.6004 7.4585L11.1671 12.8918C10.5254 13.5335 9.47539 13.5335 8.83372 12.8918L3.40039 7.4585" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
@@ -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 (
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36" fill="none" style={style}>
<rect width="36" height="36" rx="8" fill="#0D9F43" fillOpacity="0.15" />
<path d="M17.9993 26.3332C22.5827 26.3332 26.3327 22.5832 26.3327 17.9998C26.3327 13.4165 22.5827 9.6665 17.9993 9.6665C13.416 9.6665 9.66602 13.4165 9.66602 17.9998C9.66602 22.5832 13.416 26.3332 17.9993 26.3332Z" stroke="#3C9A4F" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.459 17.9999L16.8173 20.3583L21.5423 15.6416" stroke="#3C9A4F" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<rect width="36" height="36" rx="8" style={{ fill: 'var(--success)', fillOpacity: 0.15 }}/>
<path d="M17.9993 26.3332C22.5827 26.3332 26.3327 22.5832 26.3327 17.9998C26.3327 13.4165 22.5827 9.6665 17.9993 9.6665C13.416 9.6665 9.66602 13.4165 9.66602 17.9998C9.66602 22.5832 13.416 26.3332 17.9993 26.3332Z" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--success)' }}/>
<path d="M14.459 17.9999L16.8173 20.3583L21.5423 15.6416" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--success)' }}/>
</svg>
);
}
export function FilesServiceMore({ style }: { style?: CSSProperties }) {
export function FilesServiceMore({ style }: { style?: CSSProperties }){
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" style={style}>
<path d="M4.16667 8.3335C3.25 8.3335 2.5 9.0835 2.5 10.0002C2.5 10.9168 3.25 11.6668 4.16667 11.6668C5.08333 11.6668 5.83333 10.9168 5.83333 10.0002C5.83333 9.0835 5.08333 8.3335 4.16667 8.3335Z" fill="#3B3B3B" stroke="#3B3B3B" />
<path d="M15.8327 8.3335C14.916 8.3335 14.166 9.0835 14.166 10.0002C14.166 10.9168 14.916 11.6668 15.8327 11.6668C16.7493 11.6668 17.4993 10.9168 17.4993 10.0002C17.4993 9.0835 16.7493 8.3335 15.8327 8.3335Z" fill="#3B3B3B" stroke="#3B3B3B" />
<path d="M10.0007 8.3335C9.08398 8.3335 8.33398 9.0835 8.33398 10.0002C8.33398 10.9168 9.08398 11.6668 10.0007 11.6668C10.9173 11.6668 11.6673 10.9168 11.6673 10.0002C11.6673 9.0835 10.9173 8.3335 10.0007 8.3335Z" fill="#3B3B3B" stroke="#3B3B3B" />
<path d="M4.16667 8.3335C3.25 8.3335 2.5 9.0835 2.5 10.0002C2.5 10.9168 3.25 11.6668 4.16667 11.6668C5.08333 11.6668 5.83333 10.9168 5.83333 10.0002C5.83333 9.0835 5.08333 8.3335 4.16667 8.3335Z" style={{ fill: 'var(--text)', stroke: 'var(--text)' }}/>
<path d="M15.8327 8.3335C14.916 8.3335 14.166 9.0835 14.166 10.0002C14.166 10.9168 14.916 11.6668 15.8327 11.6668C16.7493 11.6668 17.4993 10.9168 17.4993 10.0002C17.4993 9.0835 16.7493 8.3335 15.8327 8.3335Z" style={{ fill: 'var(--text)', stroke: 'var(--text)' }}/>
<path d="M10.0007 8.3335C9.08398 8.3335 8.33398 9.0835 8.33398 10.0002C8.33398 10.9168 9.08398 11.6668 10.0007 11.6668C10.9173 11.6668 11.6673 10.9168 11.6673 10.0002C11.6673 9.0835 10.9173 8.3335 10.0007 8.3335Z" style={{ fill: 'var(--text)', stroke: 'var(--text)' }}/>
</svg>
);
}
@@ -33,10 +33,10 @@ export function FilesServiceMore({ style }: { style?: CSSProperties }) {
export function FilesServicePhone() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36" fill="none">
<rect width="36" height="36" rx="18" fill="#FFD8C5" fillOpacity="0.6" />
<path d="M26.3077 23.2748C26.3077 23.5748 26.241 23.8832 26.0993 24.1832C25.9577 24.4832 25.7744 24.7665 25.5327 25.0332C25.1244 25.4832 24.6743 25.8082 24.166 26.0165C23.666 26.2248 23.1244 26.3332 22.541 26.3332C21.691 26.3332 20.7827 26.1332 19.8243 25.7248C18.866 25.3165 17.9077 24.7665 16.9577 24.0748C15.9993 23.3748 15.091 22.5998 14.2243 21.7415C13.366 20.8748 12.591 19.9665 11.8993 19.0165C11.216 18.0665 10.666 17.1165 10.266 16.1748C9.86602 15.2248 9.66602 14.3165 9.66602 13.4498C9.66602 12.8832 9.76602 12.3415 9.96602 11.8415C10.166 11.3332 10.4827 10.8665 10.9243 10.4498C11.4577 9.92484 12.041 9.6665 12.6577 9.6665C12.891 9.6665 13.1243 9.7165 13.3327 9.8165C13.5493 9.9165 13.741 10.0665 13.891 10.2832L15.8243 13.0082C15.9743 13.2165 16.0827 13.4082 16.1577 13.5915C16.2327 13.7665 16.2743 13.9415 16.2743 14.0998C16.2743 14.2998 16.216 14.4998 16.0993 14.6915C15.991 14.8832 15.8327 15.0832 15.6327 15.2832L14.9993 15.9415C14.9077 16.0332 14.866 16.1415 14.866 16.2748C14.866 16.3415 14.8743 16.3998 14.891 16.4665C14.916 16.5332 14.941 16.5832 14.9577 16.6332C15.1077 16.9082 15.366 17.2665 15.7327 17.6998C16.1077 18.1332 16.5077 18.5748 16.941 19.0165C17.391 19.4582 17.8243 19.8665 18.266 20.2415C18.6993 20.6082 19.0577 20.8582 19.341 21.0082C19.3827 21.0248 19.4327 21.0498 19.491 21.0748C19.5577 21.0998 19.6243 21.1082 19.6993 21.1082C19.841 21.1082 19.9493 21.0582 20.041 20.9665L20.6743 20.3415C20.8827 20.1332 21.0827 19.9748 21.2743 19.8748C21.466 19.7582 21.6577 19.6998 21.866 19.6998C22.0243 19.6998 22.191 19.7332 22.3743 19.8082C22.5577 19.8832 22.7494 19.9915 22.9577 20.1332L25.716 22.0915C25.9327 22.2415 26.0827 22.4165 26.1743 22.6248C26.2577 22.8332 26.3077 23.0415 26.3077 23.2748Z" stroke="#F17732" strokeWidth="1.5" strokeMiterlimit="10" />
<path d="M23.4167 15.5002C23.4167 15.0002 23.025 14.2335 22.4417 13.6085C21.9083 13.0335 21.2 12.5835 20.5 12.5835" stroke="#F17732" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M26.3333 15.4998C26.3333 12.2748 23.725 9.6665 20.5 9.6665" stroke="#F17732" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<rect width="36" height="36" rx="18" style={{ fill: 'var(--accent-bg)', fillOpacity: 0.6 }}/>
<path d="M26.3077 23.2748C26.3077 23.5748 26.241 23.8832 26.0993 24.1832C25.9577 24.4832 25.7744 24.7665 25.5327 25.0332C25.1244 25.4832 24.6743 25.8082 24.166 26.0165C23.666 26.2248 23.1244 26.3332 22.541 26.3332C21.691 26.3332 20.7827 26.1332 19.8243 25.7248C18.866 25.3165 17.9077 24.7665 16.9577 24.0748C15.9993 23.3748 15.091 22.5998 14.2243 21.7415C13.366 20.8748 12.591 19.9665 11.8993 19.0165C11.216 18.0665 10.666 17.1165 10.266 16.1748C9.86602 15.2248 9.66602 14.3165 9.66602 13.4498C9.66602 12.8832 9.76602 12.3415 9.96602 11.8415C10.166 11.3332 10.4827 10.8665 10.9243 10.4498C11.4577 9.92484 12.041 9.6665 12.6577 9.6665C12.891 9.6665 13.1243 9.7165 13.3327 9.8165C13.5493 9.9165 13.741 10.0665 13.891 10.2832L15.8243 13.0082C15.9743 13.2165 16.0827 13.4082 16.1577 13.5915C16.2327 13.7665 16.2743 13.9415 16.2743 14.0998C16.2743 14.2998 16.216 14.4998 16.0993 14.6915C15.991 14.8832 15.8327 15.0832 15.6327 15.2832L14.9993 15.9415C14.9077 16.0332 14.866 16.1415 14.866 16.2748C14.866 16.3415 14.8743 16.3998 14.891 16.4665C14.916 16.5332 14.941 16.5832 14.9577 16.6332C15.1077 16.9082 15.366 17.2665 15.7327 17.6998C16.1077 18.1332 16.5077 18.5748 16.941 19.0165C17.391 19.4582 17.8243 19.8665 18.266 20.2415C18.6993 20.6082 19.0577 20.8582 19.341 21.0082C19.3827 21.0248 19.4327 21.0498 19.491 21.0748C19.5577 21.0998 19.6243 21.1082 19.6993 21.1082C19.841 21.1082 19.9493 21.0582 20.041 20.9665L20.6743 20.3415C20.8827 20.1332 21.0827 19.9748 21.2743 19.8748C21.466 19.7582 21.6577 19.6998 21.866 19.6998C22.0243 19.6998 22.191 19.7332 22.3743 19.8082C22.5577 19.8832 22.7494 19.9915 22.9577 20.1332L25.716 22.0915C25.9327 22.2415 26.0827 22.4165 26.1743 22.6248C26.2577 22.8332 26.3077 23.0415 26.3077 23.2748Z" strokeWidth="1.5" strokeMiterlimit="10" style={{ stroke: 'var(--accent)' }}/>
<path d="M23.4167 15.5002C23.4167 15.0002 23.025 14.2335 22.4417 13.6085C21.9083 13.0335 21.2 12.5835 20.5 12.5835" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--accent)' }}/>
<path d="M26.3333 15.4998C26.3333 12.2748 23.725 9.6665 20.5 9.6665" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--accent)' }}/>
</svg>
);
}
@@ -44,16 +44,16 @@ export function FilesServicePhone() {
export function FilesServiceCalendar() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36" fill="none">
<rect width="36" height="36" rx="18" fill="#FFD8C5" fillOpacity="0.6" />
<path d="M14.666 9.6665V12.1665" stroke="#F17732" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M21.334 9.6665V12.1665" stroke="#F17732" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M23.1667 25.8333C24.6394 25.8333 25.8333 24.6394 25.8333 23.1667C25.8333 21.6939 24.6394 20.5 23.1667 20.5C21.6939 20.5 20.5 21.6939 20.5 23.1667C20.5 24.6394 21.6939 25.8333 23.1667 25.8333Z" stroke="#F17732" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M26.3333 26.3333L25.5 25.5" stroke="#F17732" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M10.916 15.5752H25.0827" stroke="#F17732" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M19.1417 26.3332H14.6667C11.75 26.3332 10.5 24.6665 10.5 22.1665V15.0832C10.5 12.5832 11.75 10.9165 14.6667 10.9165H21.3333C24.25 10.9165 25.5 12.5832 25.5 15.0832V18.8332" stroke="#F17732" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M17.9961 19.4167H18.0036" stroke="#F17732" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.9121 19.4167H14.9196" stroke="#F17732" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.9121 21.9167H14.9196" stroke="#F17732" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<rect width="36" height="36" rx="18" style={{ fill: 'var(--accent-bg)', fillOpacity: 0.6 }}/>
<path d="M14.666 9.6665V12.1665" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--accent)' }}/>
<path d="M21.334 9.6665V12.1665" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--accent)' }}/>
<path d="M23.1667 25.8333C24.6394 25.8333 25.8333 24.6394 25.8333 23.1667C25.8333 21.6939 24.6394 20.5 23.1667 20.5C21.6939 20.5 20.5 21.6939 20.5 23.1667C20.5 24.6394 21.6939 25.8333 23.1667 25.8333Z" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--accent)' }}/>
<path d="M26.3333 26.3333L25.5 25.5" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--accent)' }}/>
<path d="M10.916 15.5752H25.0827" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--accent)' }}/>
<path d="M19.1417 26.3332H14.6667C11.75 26.3332 10.5 24.6665 10.5 22.1665V15.0832C10.5 12.5832 11.75 10.9165 14.6667 10.9165H21.3333C24.25 10.9165 25.5 12.5832 25.5 15.0832V18.8332" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--accent)' }}/>
<path d="M17.9961 19.4167H18.0036" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--accent)' }}/>
<path d="M14.9121 19.4167H14.9196" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--accent)' }}/>
<path d="M14.9121 21.9167H14.9196" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--accent)' }}/>
</svg>
);
}
@@ -61,9 +61,9 @@ export function FilesServiceCalendar() {
export function FilesServiceNotification() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12.0199 20.5299C9.68987 20.5299 7.35987 20.1599 5.14987 19.4199C4.30987 19.1299 3.66987 18.5399 3.38987 17.7699C3.09987 16.9999 3.19987 16.1499 3.65987 15.3899L4.80987 13.4799C5.04987 13.0799 5.26987 12.2799 5.26987 11.8099V8.91992C5.26987 5.19992 8.29987 2.16992 12.0199 2.16992C15.7399 2.16992 18.7699 5.19992 18.7699 8.91992V11.8099C18.7699 12.2699 18.9899 13.0799 19.2299 13.4899L20.3699 15.3899C20.7999 16.1099 20.8799 16.9799 20.5899 17.7699C20.2999 18.5599 19.6699 19.1599 18.8799 19.4199C16.6799 20.1599 14.3499 20.5299 12.0199 20.5299Z" stroke="#F17732" strokeWidth="1.5" />
<path d="M13.8806 3.94018C13.8106 3.94018 13.7406 3.93018 13.6706 3.91018C13.3806 3.83018 13.1006 3.77018 12.8306 3.73018C11.9806 3.62018 11.1606 3.68018 10.3906 3.91018C10.1106 4.00018 9.8106 3.91018 9.6206 3.70018C9.4306 3.49018 9.3706 3.19018 9.4806 2.92018C9.8906 1.87018 10.8906 1.18018 12.0306 1.18018C13.1706 1.18018 14.1706 1.86018 14.5806 2.92018C14.6806 3.19018 14.6306 3.49018 14.4406 3.70018C14.2906 3.86018 14.0806 3.94018 13.8806 3.94018Z" stroke="#F17732" strokeWidth="1.5" />
<path d="M12.0195 22.8101C11.0295 22.8101 10.0695 22.4101 9.36953 21.7101C8.66953 21.0101 8.26953 20.0501 8.26953 19.0601H9.76953C9.76953 19.6501 10.0095 20.2301 10.4295 20.6501C10.8495 21.0701 11.4295 21.3101 12.0195 21.3101C13.2595 21.3101 14.2695 20.3001 14.2695 19.0601H15.7695C15.7695 21.1301 14.0895 22.8101 12.0195 22.8101Z" stroke="#F17732" strokeWidth="1.5" />
<path d="M12.0199 20.5299C9.68987 20.5299 7.35987 20.1599 5.14987 19.4199C4.30987 19.1299 3.66987 18.5399 3.38987 17.7699C3.09987 16.9999 3.19987 16.1499 3.65987 15.3899L4.80987 13.4799C5.04987 13.0799 5.26987 12.2799 5.26987 11.8099V8.91992C5.26987 5.19992 8.29987 2.16992 12.0199 2.16992C15.7399 2.16992 18.7699 5.19992 18.7699 8.91992V11.8099C18.7699 12.2699 18.9899 13.0799 19.2299 13.4899L20.3699 15.3899C20.7999 16.1099 20.8799 16.9799 20.5899 17.7699C20.2999 18.5599 19.6699 19.1599 18.8799 19.4199C16.6799 20.1599 14.3499 20.5299 12.0199 20.5299Z" strokeWidth="1.5" style={{ stroke: 'var(--accent)' }}/>
<path d="M13.8806 3.94018C13.8106 3.94018 13.7406 3.93018 13.6706 3.91018C13.3806 3.83018 13.1006 3.77018 12.8306 3.73018C11.9806 3.62018 11.1606 3.68018 10.3906 3.91018C10.1106 4.00018 9.8106 3.91018 9.6206 3.70018C9.4306 3.49018 9.3706 3.19018 9.4806 2.92018C9.8906 1.87018 10.8906 1.18018 12.0306 1.18018C13.1706 1.18018 14.1706 1.86018 14.5806 2.92018C14.6806 3.19018 14.6306 3.49018 14.4406 3.70018C14.2906 3.86018 14.0806 3.94018 13.8806 3.94018Z" strokeWidth="1.5" style={{ stroke: 'var(--accent)' }}/>
<path d="M12.0195 22.8101C11.0295 22.8101 10.0695 22.4101 9.36953 21.7101C8.66953 21.0101 8.26953 20.0501 8.26953 19.0601H9.76953C9.76953 19.6501 10.0095 20.2301 10.4295 20.6501C10.8495 21.0701 11.4295 21.3101 12.0195 21.3101C13.2595 21.3101 14.2695 20.3001 14.2695 19.0601H15.7695C15.7695 21.1301 14.0895 22.8101 12.0195 22.8101Z" strokeWidth="1.5" style={{ stroke: 'var(--accent)' }}/>
</svg>
);
}
@@ -71,17 +71,17 @@ export function FilesServiceNotification() {
export function FilesServiceMessage() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M7.08268 15.8332H6.66602C3.33268 15.8332 1.66602 14.9998 1.66602 10.8332V6.6665C1.66602 3.33317 3.33268 1.6665 6.66602 1.6665H13.3327C16.666 1.6665 18.3327 3.33317 18.3327 6.6665V10.8332C18.3327 14.1665 16.666 15.8332 13.3327 15.8332H12.916C12.6577 15.8332 12.4077 15.9582 12.2493 16.1665L10.9993 17.8332C10.4493 18.5665 9.54935 18.5665 8.99935 17.8332L7.74935 16.1665C7.61602 15.9832 7.30768 15.8332 7.08268 15.8332Z" stroke="#FFFFFF" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5.83398 6.6665H14.1673" stroke="#FFFFFF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5.83398 10.8335H10.834" stroke="#FFFFFF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M7.08268 15.8332H6.66602C3.33268 15.8332 1.66602 14.9998 1.66602 10.8332V6.6665C1.66602 3.33317 3.33268 1.6665 6.66602 1.6665H13.3327C16.666 1.6665 18.3327 3.33317 18.3327 6.6665V10.8332C18.3327 14.1665 16.666 15.8332 13.3327 15.8332H12.916C12.6577 15.8332 12.4077 15.9582 12.2493 16.1665L10.9993 17.8332C10.4493 18.5665 9.54935 18.5665 8.99935 17.8332L7.74935 16.1665C7.61602 15.9832 7.30768 15.8332 7.08268 15.8332Z" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--on-primary)' }}/>
<path d="M5.83398 6.6665H14.1673" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--on-primary)' }}/>
<path d="M5.83398 10.8335H10.834" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--on-primary)' }}/>
</svg>
);
}
export function ArrowLeftPH({ style }: { style?: CSSProperties }) {
export function ArrowLeftPH({ style }: { style?: CSSProperties }){
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" style={{ width: 18, height: 18, transform: 'rotate(180deg)', ...style }}>
<path d="M14.9973 19.9201L8.47728 13.4001C7.70728 12.6301 7.70728 11.3701 8.47728 10.6001L14.9973 4.08008" stroke="#616161" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.9973 19.9201L8.47728 13.4001C7.70728 12.6301 7.70728 11.3701 8.47728 10.6001L14.9973 4.08008" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--text-2)' }}/>
</svg>
);
}
@@ -89,163 +89,163 @@ export function ArrowLeftPH({ style }: { style?: CSSProperties }) {
export function ArrowLeftD() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none">
<path d="M9.57 5.93018L3.5 12.0002L9.57 18.0702" stroke="#616161" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M20.4999 12H3.66992" stroke="#616161" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M9.57 5.93018L3.5 12.0002L9.57 18.0702" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--text-2)' }}/>
<path d="M20.4999 12H3.66992" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--text-2)' }}/>
</svg>
);
}
/* ── 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 (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={style}>
<path d="M7.9668 15.0002V10.8335" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M10.0007 12.9165H5.83398" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M6.66602 1.6665V4.1665" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M13.334 1.6665V4.1665" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M13.1758 2.8501C15.9592 2.9501 17.3675 3.9751 17.4508 7.89176L17.5592 13.0334C17.6258 16.4668 16.8342 18.1918 12.6675 18.2834L7.66751 18.3834C3.50084 18.4668 2.63417 16.7668 2.56751 13.3418L2.45084 8.19176C2.36751 4.2751 3.74251 3.19176 6.50917 2.98343L13.1758 2.8501Z" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={{ color, ...style }}>
<path d="M7.9668 15.0002V10.8335" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M10.0007 12.9165H5.83398" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M6.66602 1.6665V4.1665" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M13.334 1.6665V4.1665" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M13.1758 2.8501C15.9592 2.9501 17.3675 3.9751 17.4508 7.89176L17.5592 13.0334C17.6258 16.4668 16.8342 18.1918 12.6675 18.2834L7.66751 18.3834C3.50084 18.4668 2.63417 16.7668 2.56751 13.3418L2.45084 8.19176C2.36751 4.2751 3.74251 3.19176 6.50917 2.98343L13.1758 2.8501Z" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function TabInfo({ color = '#616161', style }: IconProps) {
export function TabInfo({ color = 'currentColor', style }: IconProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={style}>
<path d="M6.66602 1.6665V4.1665" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M13.334 1.6665V4.1665" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5.83398 10.8335H12.5007" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5.83398 14.1665H10.0007" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M13.3333 2.9165C16.1083 3.0665 17.5 4.12484 17.5 8.0415V13.1915C17.5 16.6248 16.6667 18.3415 12.5 18.3415H7.5C3.33333 18.3415 2.5 16.6248 2.5 13.1915V8.0415C2.5 4.12484 3.89167 3.07484 6.66667 2.9165H13.3333Z" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={{ color, ...style }}>
<path d="M6.66602 1.6665V4.1665" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M13.334 1.6665V4.1665" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5.83398 10.8335H12.5007" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5.83398 14.1665H10.0007" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M13.3333 2.9165C16.1083 3.0665 17.5 4.12484 17.5 8.0415V13.1915C17.5 16.6248 16.6667 18.3415 12.5 18.3415H7.5C3.33333 18.3415 2.5 16.6248 2.5 13.1915V8.0415C2.5 4.12484 3.89167 3.07484 6.66667 2.9165H13.3333Z" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function TabCalendar({ color = '#616161', style }: IconProps) {
export function TabCalendar({ color = 'currentColor', style }: IconProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" style={style}>
<path d="M8 2V5" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M16 2V5" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M3.5 9.08984H20.5" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M21 8.5V17C21 20 19.5 22 16 22H8C4.5 22 3 20 3 17V8.5C3 5.5 4.5 3.5 8 3.5H16C19.5 3.5 21 5.5 21 8.5Z" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M15.6947 13.7002H15.7037" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.9955 13.7002H12.0045" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.29431 13.7002H8.30329" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" style={{ color, ...style }}>
<path d="M8 2V5" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M16 2V5" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M3.5 9.08984H20.5" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M21 8.5V17C21 20 19.5 22 16 22H8C4.5 22 3 20 3 17V8.5C3 5.5 4.5 3.5 8 3.5H16C19.5 3.5 21 5.5 21 8.5Z" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M15.6947 13.7002H15.7037" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.9955 13.7002H12.0045" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.29431 13.7002H8.30329" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function TabCard({ color = '#616161', style }: IconProps) {
export function TabCard({ color = 'currentColor', style }: IconProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={style}>
<path d="M3.27539 13.2327L13.2337 3.27441" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M9.25195 15.2324L10.252 14.2324" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.4941 12.9907L13.4858 10.999" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M3.00041 8.53258L8.53374 2.99925C10.3004 1.23258 11.1837 1.22425 12.9337 2.97425L17.0254 7.06591C18.7754 8.81591 18.7671 9.69925 17.0004 11.4659L11.4671 16.9992C9.70041 18.7659 8.81707 18.7742 7.06707 17.0242L2.97541 12.9326C1.22541 11.1826 1.22541 10.3076 3.00041 8.53258Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M1.66602 18.332H18.3327" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={{ color, ...style }}>
<path d="M3.27539 13.2327L13.2337 3.27441" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M9.25195 15.2324L10.252 14.2324" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.4941 12.9907L13.4858 10.999" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M3.00041 8.53258L8.53374 2.99925C10.3004 1.23258 11.1837 1.22425 12.9337 2.97425L17.0254 7.06591C18.7754 8.81591 18.7671 9.69925 17.0004 11.4659L11.4671 16.9992C9.70041 18.7659 8.81707 18.7742 7.06707 17.0242L2.97541 12.9326C1.22541 11.1826 1.22541 10.3076 3.00041 8.53258Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M1.66602 18.332H18.3327" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function TabWallet({ color = '#616161', style }: IconProps) {
export function TabWallet({ color = 'currentColor', style }: IconProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={style}>
<path d="M10.834 9.2915H5.83398" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M1.66602 9.2917V5.4417C1.66602 3.7417 3.04102 2.3667 4.74102 2.3667H9.42435C11.1243 2.3667 12.4993 3.42503 12.4993 5.12503" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.566 10.1667C14.1493 10.5667 13.9494 11.1833 14.116 11.8166C14.3244 12.5916 15.091 13.0833 15.891 13.0833H16.666V14.2917C16.666 16.1333 15.1743 17.625 13.3327 17.625H4.99935C3.15768 17.625 1.66602 16.1333 1.66602 14.2917V8.45833C1.66602 6.61667 3.15768 5.125 4.99935 5.125H13.3327C15.166 5.125 16.666 6.625 16.666 8.45833V9.66663H15.766C15.2993 9.66663 14.8743 9.84999 14.566 10.1665Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M18.3329 10.5165V12.2332C18.3329 12.6999 17.9495 13.0832 17.4745 13.0832H15.8662C14.9662 13.0832 14.1412 12.4249 14.0662 11.5249C14.0162 10.9999 14.2162 10.5082 14.5662 10.1665C14.8745 9.84987 15.2995 9.6665 15.7662 9.6665H17.4745C17.9495 9.6665 18.3329 10.0499 18.3329 10.5165Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={{ color, ...style }}>
<path d="M10.834 9.2915H5.83398" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M1.66602 9.2917V5.4417C1.66602 3.7417 3.04102 2.3667 4.74102 2.3667H9.42435C11.1243 2.3667 12.4993 3.42503 12.4993 5.12503" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.566 10.1667C14.1493 10.5667 13.9494 11.1833 14.116 11.8166C14.3244 12.5916 15.091 13.0833 15.891 13.0833H16.666V14.2917C16.666 16.1333 15.1743 17.625 13.3327 17.625H4.99935C3.15768 17.625 1.66602 16.1333 1.66602 14.2917V8.45833C1.66602 6.61667 3.15768 5.125 4.99935 5.125H13.3327C15.166 5.125 16.666 6.625 16.666 8.45833V9.66663H15.766C15.2993 9.66663 14.8743 9.84999 14.566 10.1665Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M18.3329 10.5165V12.2332C18.3329 12.6999 17.9495 13.0832 17.4745 13.0832H15.8662C14.9662 13.0832 14.1412 12.4249 14.0662 11.5249C14.0162 10.9999 14.2162 10.5082 14.5662 10.1665C14.8745 9.84987 15.2995 9.6665 15.7662 9.6665H17.4745C17.9495 9.6665 18.3329 10.0499 18.3329 10.5165Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function TabSMS({ color = '#616161', style }: IconProps) {
export function TabSMS({ color = 'currentColor', style }: IconProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={style}>
<path d="M14.166 17.0832H5.83268C3.33268 17.0832 1.66602 15.8332 1.66602 12.9165V7.08317C1.66602 4.1665 3.33268 2.9165 5.83268 2.9165H14.166C16.666 2.9165 18.3327 4.1665 18.3327 7.08317V12.9165C18.3327 15.8332 16.666 17.0832 14.166 17.0832Z" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.1673 7.5L11.559 9.58333C10.7006 10.2667 9.29231 10.2667 8.43398 9.58333L5.83398 7.5" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={{ color, ...style }}>
<path d="M14.166 17.0832H5.83268C3.33268 17.0832 1.66602 15.8332 1.66602 12.9165V7.08317C1.66602 4.1665 3.33268 2.9165 5.83268 2.9165H14.166C16.666 2.9165 18.3327 4.1665 18.3327 7.08317V12.9165C18.3327 15.8332 16.666 17.0832 14.166 17.0832Z" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.1673 7.5L11.559 9.58333C10.7006 10.2667 9.29231 10.2667 8.43398 9.58333L5.83398 7.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function TabCall({ color = '#616161', style }: IconProps) {
export function TabCall({ color = 'currentColor', style }: IconProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={style}>
<path d="M18.3077 15.2748C18.3077 15.5748 18.241 15.8832 18.0993 16.1832C17.9577 16.4832 17.7744 16.7665 17.5327 17.0332C17.1244 17.4832 16.6743 17.8082 16.166 18.0165C15.666 18.2248 15.1244 18.3332 14.541 18.3332C13.691 18.3332 12.7827 18.1332 11.8243 17.7248C10.866 17.3165 9.90768 16.7665 8.95768 16.0748C7.99935 15.3748 7.09102 14.5998 6.22435 13.7415C5.36602 12.8748 4.59102 11.9665 3.89935 11.0165C3.21602 10.0665 2.66602 9.1165 2.26602 8.17484C1.86602 7.22484 1.66602 6.3165 1.66602 5.44984C1.66602 4.88317 1.76602 4.3415 1.96602 3.8415C2.16602 3.33317 2.48268 2.8665 2.92435 2.44984C3.45768 1.92484 4.04102 1.6665 4.65768 1.6665C4.89102 1.6665 5.12435 1.7165 5.33268 1.8165C5.54935 1.9165 5.74102 2.0665 5.89102 2.28317L7.82435 5.00817C7.97435 5.2165 8.08268 5.40817 8.15768 5.5915C8.23268 5.7665 8.27435 5.9415 8.27435 6.09984C8.27435 6.29984 8.21602 6.49984 8.09935 6.6915C7.99102 6.88317 7.83268 7.08317 7.63268 7.28317L6.99935 7.9415C6.90768 8.03317 6.86602 8.1415 6.86602 8.27484C6.86602 8.3415 6.87435 8.39984 6.89102 8.4665C6.91602 8.53317 6.94102 8.58317 6.95768 8.63317C7.10768 8.90817 7.36602 9.2665 7.73268 9.69984C8.10768 10.1332 8.50768 10.5748 8.94102 11.0165C9.39102 11.4582 9.82435 11.8665 10.266 12.2415C10.6993 12.6082 11.0577 12.8582 11.341 13.0082C11.3827 13.0248 11.4327 13.0498 11.491 13.0748C11.5577 13.0998 11.6243 13.1082 11.6993 13.1082C11.841 13.1082 11.9493 13.0582 12.041 12.9665L12.6743 12.3415C12.8827 12.1332 13.0827 11.9748 13.2743 11.8748C13.466 11.7582 13.6577 11.6998 13.866 11.6998C14.0243 11.6998 14.191 11.7332 14.3743 11.8082C14.5577 11.8832 14.7494 11.9915 14.9577 12.1332L17.716 14.0915C17.9327 14.2415 18.0827 14.4165 18.1743 14.6248C18.2577 14.8332 18.3077 15.0415 18.3077 15.2748Z" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" />
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={{ color, ...style }}>
<path d="M18.3077 15.2748C18.3077 15.5748 18.241 15.8832 18.0993 16.1832C17.9577 16.4832 17.7744 16.7665 17.5327 17.0332C17.1244 17.4832 16.6743 17.8082 16.166 18.0165C15.666 18.2248 15.1244 18.3332 14.541 18.3332C13.691 18.3332 12.7827 18.1332 11.8243 17.7248C10.866 17.3165 9.90768 16.7665 8.95768 16.0748C7.99935 15.3748 7.09102 14.5998 6.22435 13.7415C5.36602 12.8748 4.59102 11.9665 3.89935 11.0165C3.21602 10.0665 2.66602 9.1165 2.26602 8.17484C1.86602 7.22484 1.66602 6.3165 1.66602 5.44984C1.66602 4.88317 1.76602 4.3415 1.96602 3.8415C2.16602 3.33317 2.48268 2.8665 2.92435 2.44984C3.45768 1.92484 4.04102 1.6665 4.65768 1.6665C4.89102 1.6665 5.12435 1.7165 5.33268 1.8165C5.54935 1.9165 5.74102 2.0665 5.89102 2.28317L7.82435 5.00817C7.97435 5.2165 8.08268 5.40817 8.15768 5.5915C8.23268 5.7665 8.27435 5.9415 8.27435 6.09984C8.27435 6.29984 8.21602 6.49984 8.09935 6.6915C7.99102 6.88317 7.83268 7.08317 7.63268 7.28317L6.99935 7.9415C6.90768 8.03317 6.86602 8.1415 6.86602 8.27484C6.86602 8.3415 6.87435 8.39984 6.89102 8.4665C6.91602 8.53317 6.94102 8.58317 6.95768 8.63317C7.10768 8.90817 7.36602 9.2665 7.73268 9.69984C8.10768 10.1332 8.50768 10.5748 8.94102 11.0165C9.39102 11.4582 9.82435 11.8665 10.266 12.2415C10.6993 12.6082 11.0577 12.8582 11.341 13.0082C11.3827 13.0248 11.4327 13.0498 11.491 13.0748C11.5577 13.0998 11.6243 13.1082 11.6993 13.1082C11.841 13.1082 11.9493 13.0582 12.041 12.9665L12.6743 12.3415C12.8827 12.1332 13.0827 11.9748 13.2743 11.8748C13.466 11.7582 13.6577 11.6998 13.866 11.6998C14.0243 11.6998 14.191 11.7332 14.3743 11.8082C14.5577 11.8832 14.7494 11.9915 14.9577 12.1332L17.716 14.0915C17.9327 14.2415 18.0827 14.4165 18.1743 14.6248C18.2577 14.8332 18.3077 15.0415 18.3077 15.2748Z" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" />
</svg>
);
}
export function TabAttach({ color = '#616161', style }: IconProps) {
export function TabAttach({ color = 'currentColor', style }: IconProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={style}>
<path d="M10.1664 9.83352L8.99138 11.0085C8.34138 11.6585 8.34138 12.7169 8.99138 13.3669C9.64138 14.0169 10.6997 14.0169 11.3497 13.3669L13.1998 11.5169C14.4998 10.2169 14.4998 8.10853 13.1998 6.8002C11.8998 5.5002 9.79139 5.5002 8.48306 6.8002L6.46641 8.81685C5.34974 9.93351 5.34974 11.7419 6.46641 12.8585" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M7.49935 18.3332H12.4993C16.666 18.3332 18.3327 16.6665 18.3327 12.4998V7.49984C18.3327 3.33317 16.666 1.6665 12.4993 1.6665H7.49935C3.33268 1.6665 1.66602 3.33317 1.66602 7.49984V12.4998C1.66602 16.6665 3.33268 18.3332 7.49935 18.3332Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={{ color, ...style }}>
<path d="M10.1664 9.83352L8.99138 11.0085C8.34138 11.6585 8.34138 12.7169 8.99138 13.3669C9.64138 14.0169 10.6997 14.0169 11.3497 13.3669L13.1998 11.5169C14.4998 10.2169 14.4998 8.10853 13.1998 6.8002C11.8998 5.5002 9.79139 5.5002 8.48306 6.8002L6.46641 8.81685C5.34974 9.93351 5.34974 11.7419 6.46641 12.8585" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M7.49935 18.3332H12.4993C16.666 18.3332 18.3327 16.6665 18.3327 12.4998V7.49984C18.3327 3.33317 16.666 1.6665 12.4993 1.6665H7.49935C3.33268 1.6665 1.66602 3.33317 1.66602 7.49984V12.4998C1.66602 16.6665 3.33268 18.3332 7.49935 18.3332Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function TabBody({ color = '#616161', style }: IconProps) {
export function TabBody({ color = 'currentColor', style }: IconProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={style}>
<path d="M5 6.6665C3.75 6.66651 3.75 8.33317 5 8.33317L6.6671 8.33317C7.12734 8.33317 7.5 8.70357 7.5 9.16381V17.0832C7.5 18.3332 9.16667 18.3332 9.16667 17.0832C9.16667 15.8332 9.16667 13.3332 10 13.3332C10.8333 13.3332 10.8333 15.8332 10.8333 17.0832C10.8333 18.3332 12.5 18.3332 12.5 17.0832V9.1665C12.5 8.70627 12.8731 8.33317 13.3333 8.33317H15C16.25 8.33317 16.25 6.6665 15 6.6665H5Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.6673 3.75016C11.6673 4.67064 10.9211 5.41683 10.0007 5.41683C9.08018 5.41683 8.33398 4.67064 8.33398 3.75016C8.33398 2.82969 9.08018 2.0835 10.0007 2.0835C10.9211 2.0835 11.6673 2.82969 11.6673 3.75016Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" style={{ color, ...style }}>
<path d="M5 6.6665C3.75 6.66651 3.75 8.33317 5 8.33317L6.6671 8.33317C7.12734 8.33317 7.5 8.70357 7.5 9.16381V17.0832C7.5 18.3332 9.16667 18.3332 9.16667 17.0832C9.16667 15.8332 9.16667 13.3332 10 13.3332C10.8333 13.3332 10.8333 15.8332 10.8333 17.0832C10.8333 18.3332 12.5 18.3332 12.5 17.0832V9.1665C12.5 8.70627 12.8731 8.33317 13.3333 8.33317H15C16.25 8.33317 16.25 6.6665 15 6.6665H5Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.6673 3.75016C11.6673 4.67064 10.9211 5.41683 10.0007 5.41683C9.08018 5.41683 8.33398 4.67064 8.33398 3.75016C8.33398 2.82969 9.08018 2.0835 10.0007 2.0835C10.9211 2.0835 11.6673 2.82969 11.6673 3.75016Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
/** 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 (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none" style={style}>
<path d="M7.49984 18.3332H12.4998C16.6665 18.3332 18.3332 16.6665 18.3332 12.4998V7.49984C18.3332 3.33317 16.6665 1.6665 12.4998 1.6665H7.49984C3.33317 1.6665 1.6665 3.33317 1.6665 7.49984V12.4998C1.6665 16.6665 3.33317 18.3332 7.49984 18.3332Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M6.4585 9.99993L8.81683 12.3583L13.5418 7.6416" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none" style={{ color, ...style }}>
<path d="M7.49984 18.3332H12.4998C16.6665 18.3332 18.3332 16.6665 18.3332 12.4998V7.49984C18.3332 3.33317 16.6665 1.6665 12.4998 1.6665H7.49984C3.33317 1.6665 1.6665 3.33317 1.6665 7.49984V12.4998C1.6665 16.6665 3.33317 18.3332 7.49984 18.3332Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M6.4585 9.99993L8.81683 12.3583L13.5418 7.6416" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
/* ── 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 (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" style={style}>
<path d="M8 2V5" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M16 2V5" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M3.5 9.08984H20.5" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M21 8.5V17C21 20 19.5 22 16 22H8C4.5 22 3 20 3 17V8.5C3 5.5 4.5 3.5 8 3.5H16C19.5 3.5 21 5.5 21 8.5Z" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M15.6947 13.7002H15.7037" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M15.6947 16.7002H15.7037" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.9955 13.7002H12.0045" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.9955 16.7002H12.0045" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.29431 13.7002H8.30329" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.29431 16.7002H8.30329" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" style={{ color, ...style }}>
<path d="M8 2V5" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M16 2V5" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M3.5 9.08984H20.5" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M21 8.5V17C21 20 19.5 22 16 22H8C4.5 22 3 20 3 17V8.5C3 5.5 4.5 3.5 8 3.5H16C19.5 3.5 21 5.5 21 8.5Z" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M15.6947 13.7002H15.7037" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M15.6947 16.7002H15.7037" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.9955 13.7002H12.0045" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.9955 16.7002H12.0045" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.29431 13.7002H8.30329" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.29431 16.7002H8.30329" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function ClockP({ color = '#525252', size = 20, style }: IconProps & { size?: number }) {
export function ClockP({ color = 'currentColor', size = 20, style }: IconProps & { size?: number }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none" style={style}>
<path d="M18.3337 10.0003C18.3337 14.6003 14.6003 18.3337 10.0003 18.3337C5.40033 18.3337 1.66699 14.6003 1.66699 10.0003C1.66699 5.40033 5.40033 1.66699 10.0003 1.66699C14.6003 1.66699 18.3337 5.40033 18.3337 10.0003Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M13.0914 12.6505L10.5081 11.1088C10.0581 10.8421 9.69141 10.2005 9.69141 9.67546V6.25879" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none" style={{ color, ...style }}>
<path d="M18.3337 10.0003C18.3337 14.6003 14.6003 18.3337 10.0003 18.3337C5.40033 18.3337 1.66699 14.6003 1.66699 10.0003C1.66699 5.40033 5.40033 1.66699 10.0003 1.66699C14.6003 1.66699 18.3337 5.40033 18.3337 10.0003Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M13.0914 12.6505L10.5081 11.1088C10.0581 10.8421 9.69141 10.2005 9.69141 9.67546V6.25879" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function UserD({ color = '#616161', size = 20, style }: IconProps & { size?: number }) {
export function UserD({ color = 'currentColor', size = 20, style }: IconProps & { size?: number }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" style={style}>
<path d="M12 12C14.7614 12 17 9.76142 17 7C17 4.23858 14.7614 2 12 2C9.23858 2 7 4.23858 7 7C7 9.76142 9.23858 12 12 12Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M20.5899 22C20.5899 18.13 16.7399 15 11.9999 15C7.25991 15 3.40991 18.13 3.40991 22" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" style={{ color, ...style }}>
<path d="M12 12C14.7614 12 17 9.76142 17 7C17 4.23858 14.7614 2 12 2C9.23858 2 7 4.23858 7 7C7 9.76142 9.23858 12 12 12Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M20.5899 22C20.5899 18.13 16.7399 15 11.9999 15C7.25991 15 3.40991 18.13 3.40991 22" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function StatusGlobe({ color = '#616161', size = 20, style }: IconProps & { size?: number }) {
export function StatusGlobe({ color = 'currentColor', size = 20, style }: IconProps & { size?: number }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none" style={style}>
<path d="M2.04166 12.4751C2.93332 15.3418 5.33333 17.5501 8.31666 18.1584" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M1.70834 9.14984C2.13334 4.9415 5.68334 1.6665 10 1.6665C14.3167 1.6665 17.8667 4.94984 18.2917 9.14984" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.675 18.1666C14.65 17.5583 17.0417 15.3749 17.95 12.5166" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none" style={{ color, ...style }}>
<path d="M2.04166 12.4751C2.93332 15.3418 5.33333 17.5501 8.31666 18.1584" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M1.70834 9.14984C2.13334 4.9415 5.68334 1.6665 10 1.6665C14.3167 1.6665 17.8667 4.94984 18.2917 9.14984" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.675 18.1666C14.65 17.5583 17.0417 15.3749 17.95 12.5166" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
@@ -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 (
<svg width={size} height={size} viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ display: 'block' }}>
<circle cx="26" cy="26" r="25" stroke={circleStrokeColor} strokeWidth="2" strokeDasharray="16 2" />
<rect x="6.5" y="6.5" width="39" height="39" rx="19.5" fill={bgColor} />
<rect x="6.5" y="6.5" width="39" height="39" rx="19.5" stroke={bgColor} />
<path d="M23.5605 32V27" stroke={iconColor} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M26 29.5H21" stroke={iconColor} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M22 16V19" stroke={iconColor} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M30 16V19" stroke={iconColor} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M29.8098 17.4199C33.1498 17.5399 34.8398 18.7699 34.9398 23.4699L35.0698 29.6399C35.1498 33.7599 34.1998 35.8299 29.1998 35.9399L23.1998 36.0599C18.1998 36.1599 17.1598 34.1199 17.0798 30.0099L16.9398 23.8299C16.8398 19.1299 18.4898 17.8299 21.8098 17.5799L29.8098 17.4199Z" stroke={iconColor} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<circle cx="26" cy="26" r="25" strokeWidth="2" strokeDasharray="16 2" style={{ stroke: circleStrokeColor }} />
<rect x="6.5" y="6.5" width="39" height="39" rx="19.5" style={{ fill: bgColor }} />
<rect x="6.5" y="6.5" width="39" height="39" rx="19.5" style={{ stroke: bgColor }} />
<path d="M23.5605 32V27" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: iconColor }} />
<path d="M26 29.5H21" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: iconColor }} />
<path d="M22 16V19" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: iconColor }} />
<path d="M30 16V19" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: iconColor }} />
<path d="M29.8098 17.4199C33.1498 17.5399 34.8398 18.7699 34.9398 23.4699L35.0698 29.6399C35.1498 33.7599 34.1998 35.8299 29.1998 35.9399L23.1998 36.0599C18.1998 36.1599 17.1598 34.1199 17.0798 30.0099L16.9398 23.8299C16.8398 19.1299 18.4898 17.8299 21.8098 17.5799L29.8098 17.4199Z" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: iconColor }} />
</svg>
);
}
/** 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 (
<svg width={size} height={size} viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ display: 'block' }}>
<circle cx="26" cy="26" r="25" stroke={circleStrokeColor} strokeWidth="2" strokeDasharray="16 2" />
<rect x="6.5" y="6.5" width="39" height="39" rx="19.5" fill={bgColor} />
<rect x="6.5" y="6.5" width="39" height="39" rx="19.5" stroke={bgColor} />
<path d="M17.4297 29.8792L29.3797 17.9292" stroke={iconColor} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M24.6016 32.2791L25.8016 31.0791" stroke={iconColor} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M27.293 29.5887L29.683 27.1987" stroke={iconColor} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M17.1013 24.239L23.7413 17.599C25.8613 15.479 26.9213 15.469 29.0213 17.569L33.9313 22.479C36.0313 24.579 36.0213 25.639 33.9013 27.759L27.2613 34.399C25.1413 36.519 24.0813 36.529 21.9813 34.429L17.0713 29.519C14.9713 27.419 14.9713 26.369 17.1013 24.239Z" stroke={iconColor} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M15.5 35.9985H35.5" stroke={iconColor} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<circle cx="26" cy="26" r="25" strokeWidth="2" strokeDasharray="16 2" style={{ stroke: circleStrokeColor }} />
<rect x="6.5" y="6.5" width="39" height="39" rx="19.5" style={{ fill: bgColor }} />
<rect x="6.5" y="6.5" width="39" height="39" rx="19.5" style={{ stroke: bgColor }} />
<path d="M17.4297 29.8792L29.3797 17.9292" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: iconColor }} />
<path d="M24.6016 32.2791L25.8016 31.0791" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: iconColor }} />
<path d="M27.293 29.5887L29.683 27.1987" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: iconColor }} />
<path d="M17.1013 24.239L23.7413 17.599C25.8613 15.479 26.9213 15.469 29.0213 17.569L33.9313 22.479C36.0313 24.579 36.0213 25.639 33.9013 27.759L27.2613 34.399C25.1413 36.519 24.0813 36.529 21.9813 34.429L17.0713 29.519C14.9713 27.419 14.9713 26.369 17.1013 24.239Z" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: iconColor }} />
<path d="M15.5 35.9985H35.5" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: iconColor }} />
</svg>
);
}
/** 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 (
<svg width={size} height={size} viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ display: 'block' }}>
<circle cx="26" cy="26" r="25" stroke={circleStrokeColor} strokeWidth="2" strokeDasharray="16 2" />
<rect x="6.5" y="6.5" width="39" height="39" rx="19.5" fill={bgColor} />
<rect x="6.5" y="6.5" width="39" height="39" rx="19.5" stroke={bgColor} />
<path d="M22 16V19" stroke={iconColor} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M30 16V19" stroke={iconColor} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M21 27H29" stroke={iconColor} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M21 31H26" stroke={iconColor} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M30 17.5C33.33 17.68 35 18.95 35 23.65V29.83C35 33.95 34 36.01 29 36.01H23C18 36.01 17 33.95 17 29.83V23.65C17 18.95 18.67 17.69 22 17.5H30Z" stroke={iconColor} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<circle cx="26" cy="26" r="25" strokeWidth="2" strokeDasharray="16 2" style={{ stroke: circleStrokeColor }} />
<rect x="6.5" y="6.5" width="39" height="39" rx="19.5" style={{ fill: bgColor }} />
<rect x="6.5" y="6.5" width="39" height="39" rx="19.5" style={{ stroke: bgColor }} />
<path d="M22 16V19" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: iconColor }} />
<path d="M30 16V19" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: iconColor }} />
<path d="M21 27H29" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: iconColor }} />
<path d="M21 31H26" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: iconColor }} />
<path d="M30 17.5C33.33 17.68 35 18.95 35 23.65V29.83C35 33.95 34 36.01 29 36.01H23C18 36.01 17 33.95 17 29.83V23.65C17 18.95 18.67 17.69 22 17.5H30Z" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: iconColor }} />
</svg>
);
}
@@ -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 (
<svg width={size} height={size} viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="20" fill="#FFF4E5" />
<path d="M27 23.5C28.9339 23.5 30.5 25.0661 30.5 27C30.5 27.6563 30.316 28.2758 29.9941 28.7979L29.9902 28.8047C29.387 29.8186 28.2772 30.5 27 30.5C25.8025 30.5 24.7525 29.9008 24.1289 28.9902L24.0098 28.8047L24.0059 28.7979L23.8916 28.5977C23.6407 28.1204 23.5 27.5743 23.5 27C23.5 25.0661 25.0661 23.5 27 23.5ZM29.4756 25.1689C29.0107 24.6708 28.216 24.6333 27.7109 25.1025L26.4629 26.2559L26.3232 26.1162C25.838 25.6313 25.0418 25.6313 24.5566 26.1162C24.0714 26.6015 24.0714 27.3985 24.5566 27.8838L25.5469 28.874C25.7954 29.1223 26.1145 29.2402 26.4297 29.2402C26.7311 29.2402 27.0438 29.1255 27.2793 28.9072L29.4092 26.9375L29.418 26.9297C29.9048 26.459 29.9501 25.6774 29.4756 25.1689Z" fill="#F17732" stroke="#F17732" />
<path d="M14.1396 11.8999H25.8496C27.8635 11.8999 29.5 13.5364 29.5 15.5503V15.9995C29.5 16.2734 29.2739 16.4995 29 16.4995H11C10.7261 16.4995 10.5 16.2734 10.5 15.9995V15.5396C10.5002 13.526 12.1261 11.9001 14.1396 11.8999Z" fill="#F17732" stroke="#F17732" />
<path d="M29 18.9902C29.2739 18.9902 29.5 19.2164 29.5 19.4902V20.8799C29.5 21.2157 29.1673 21.4608 28.8301 21.3467H28.8291C27.6616 20.9539 26.3422 20.9004 24.9873 21.334C24.1629 21.5978 23.4064 22.0584 22.792 22.6611C21.4095 24.003 20.8496 25.6792 20.9111 27.2607L20.9316 27.5762C20.9516 27.831 20.7192 28.1004 20.4004 28.1006H14.1396C12.126 28.1004 10.5 26.4737 10.5 24.46V19.5C10.5 19.2239 10.7239 19 11 18.9902H29ZM14 23.25C13.3139 23.25 12.75 23.814 12.75 24.5C12.75 25.1861 13.3139 25.75 14 25.75H16C16.6861 25.75 17.25 25.1861 17.25 24.5C17.25 23.814 16.6861 23.25 16 23.25H14Z" fill="#F17732" stroke="#F17732" />
<rect width="40" height="40" rx="20" style={{ fill: 'var(--warning-bg)' }}/>
<path d="M27 23.5C28.9339 23.5 30.5 25.0661 30.5 27C30.5 27.6563 30.316 28.2758 29.9941 28.7979L29.9902 28.8047C29.387 29.8186 28.2772 30.5 27 30.5C25.8025 30.5 24.7525 29.9008 24.1289 28.9902L24.0098 28.8047L24.0059 28.7979L23.8916 28.5977C23.6407 28.1204 23.5 27.5743 23.5 27C23.5 25.0661 25.0661 23.5 27 23.5ZM29.4756 25.1689C29.0107 24.6708 28.216 24.6333 27.7109 25.1025L26.4629 26.2559L26.3232 26.1162C25.838 25.6313 25.0418 25.6313 24.5566 26.1162C24.0714 26.6015 24.0714 27.3985 24.5566 27.8838L25.5469 28.874C25.7954 29.1223 26.1145 29.2402 26.4297 29.2402C26.7311 29.2402 27.0438 29.1255 27.2793 28.9072L29.4092 26.9375L29.418 26.9297C29.9048 26.459 29.9501 25.6774 29.4756 25.1689Z" style={{ fill: 'var(--accent)', stroke: 'var(--accent)' }}/>
<path d="M14.1396 11.8999H25.8496C27.8635 11.8999 29.5 13.5364 29.5 15.5503V15.9995C29.5 16.2734 29.2739 16.4995 29 16.4995H11C10.7261 16.4995 10.5 16.2734 10.5 15.9995V15.5396C10.5002 13.526 12.1261 11.9001 14.1396 11.8999Z" style={{ fill: 'var(--accent)', stroke: 'var(--accent)' }}/>
<path d="M29 18.9902C29.2739 18.9902 29.5 19.2164 29.5 19.4902V20.8799C29.5 21.2157 29.1673 21.4608 28.8301 21.3467H28.8291C27.6616 20.9539 26.3422 20.9004 24.9873 21.334C24.1629 21.5978 23.4064 22.0584 22.792 22.6611C21.4095 24.003 20.8496 25.6792 20.9111 27.2607L20.9316 27.5762C20.9516 27.831 20.7192 28.1004 20.4004 28.1006H14.1396C12.126 28.1004 10.5 26.4737 10.5 24.46V19.5C10.5 19.2239 10.7239 19 11 18.9902H29ZM14 23.25C13.3139 23.25 12.75 23.814 12.75 24.5C12.75 25.1861 13.3139 25.75 14 25.75H16C16.6861 25.75 17.25 25.1861 17.25 24.5C17.25 23.814 16.6861 23.25 16 23.25H14Z" style={{ fill: 'var(--accent)', stroke: 'var(--accent)' }}/>
</svg>
);
}
/** «موجودی کیف پول» icon — tauri FilesServiceBalanceWallet, verbatim. */
export function FilesServiceBalanceWallet({ size = 20 }: { size?: number }) {
export function FilesServiceBalanceWallet({ size = 20 }: { size?: number }){
return (
<svg width={size} height={size} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.833 9.2915H5.83301" stroke="#F17732" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M1.66699 9.2917V5.4417C1.66699 3.7417 3.04199 2.3667 4.74199 2.3667H9.42532C11.1253 2.3667 12.5003 3.42503 12.5003 5.12503" stroke="#F17732" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.567 10.1667C14.1503 10.5667 13.9503 11.1833 14.117 11.8166C14.3253 12.5916 15.092 13.0833 15.892 13.0833H16.667V14.2917C16.667 16.1333 15.1753 17.625 13.3337 17.625H5.00033C3.15866 17.625 1.66699 16.1333 1.66699 14.2917V8.45833C1.66699 6.61667 3.15866 5.125 5.00033 5.125H13.3337C15.167 5.125 16.667 6.625 16.667 8.45833V9.66663H15.767C15.3003 9.66663 14.8753 9.84999 14.567 10.1667Z" stroke="#F17732" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M18.3338 10.5165V12.2332C18.3338 12.6999 17.9505 13.0832 17.4755 13.0832H15.8672C14.9672 13.0832 14.1422 12.4249 14.0672 11.5249C14.0172 10.9999 14.2172 10.5082 14.5672 10.1665C14.8755 9.84987 15.3005 9.6665 15.7672 9.6665H17.4755C17.9505 9.6665 18.3338 10.0499 18.3338 10.5165Z" stroke="#F17732" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M10.833 9.2915H5.83301" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--accent)' }}/>
<path d="M1.66699 9.2917V5.4417C1.66699 3.7417 3.04199 2.3667 4.74199 2.3667H9.42532C11.1253 2.3667 12.5003 3.42503 12.5003 5.12503" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--accent)' }}/>
<path d="M14.567 10.1667C14.1503 10.5667 13.9503 11.1833 14.117 11.8166C14.3253 12.5916 15.092 13.0833 15.892 13.0833H16.667V14.2917C16.667 16.1333 15.1753 17.625 13.3337 17.625H5.00033C3.15866 17.625 1.66699 16.1333 1.66699 14.2917V8.45833C1.66699 6.61667 3.15866 5.125 5.00033 5.125H13.3337C15.167 5.125 16.667 6.625 16.667 8.45833V9.66663H15.767C15.3003 9.66663 14.8753 9.84999 14.567 10.1667Z" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--accent)' }}/>
<path d="M18.3338 10.5165V12.2332C18.3338 12.6999 17.9505 13.0832 17.4755 13.0832H15.8672C14.9672 13.0832 14.1422 12.4249 14.0672 11.5249C14.0172 10.9999 14.2172 10.5082 14.5672 10.1665C14.8755 9.84987 15.3005 9.6665 15.7672 9.6665H17.4755C17.9505 9.6665 18.3338 10.0499 18.3338 10.5165Z" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--accent)' }}/>
</svg>
);
}
/** 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 (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none">
<path d="M17.5 4.98356C14.725 4.70856 11.9333 4.56689 9.15 4.56689C7.5 4.56689 5.85 4.65023 4.2 4.81689L2.5 4.98356" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M7.08301 4.1415L7.26634 3.04984C7.39967 2.25817 7.49967 1.6665 8.90801 1.6665H11.0913C12.4997 1.6665 12.608 2.2915 12.733 3.05817L12.9163 4.1415" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M15.7087 7.6167L15.167 16.0084C15.0753 17.3167 15.0003 18.3334 12.6753 18.3334H7.32533C5.00033 18.3334 4.92533 17.3167 4.83366 16.0084L4.29199 7.6167" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.6084 13.75H11.3834" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M7.91699 10.4165H12.0837" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none" style={{ color }}>
<path d="M17.5 4.98356C14.725 4.70856 11.9333 4.56689 9.15 4.56689C7.5 4.56689 5.85 4.65023 4.2 4.81689L2.5 4.98356" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M7.08301 4.1415L7.26634 3.04984C7.39967 2.25817 7.49967 1.6665 8.90801 1.6665H11.0913C12.4997 1.6665 12.608 2.2915 12.733 3.05817L12.9163 4.1415" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M15.7087 7.6167L15.167 16.0084C15.0753 17.3167 15.0003 18.3334 12.6753 18.3334H7.32533C5.00033 18.3334 4.92533 17.3167 4.83366 16.0084L4.29199 7.6167" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.6084 13.75H11.3834" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M7.91699 10.4165H12.0837" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
@@ -354,33 +354,33 @@ export function TrashRed({ size = 20, color = '#FF5450' }: { size?: number; colo
export function CloseModalD() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 17 17" fill="none">
<path className="fill-[#7E7E7E] dark:fill-[#FAFAFA]" d="M12.5947 4.20135C12.533 4.13954 12.4597 4.09051 12.3791 4.05706C12.2984 4.0236 12.212 4.00638 12.1247 4.00638C12.0374 4.00638 11.9509 4.0236 11.8703 4.05706C11.7896 4.09051 11.7164 4.13954 11.6547 4.20135L8.39468 7.45468L5.13468 4.19468C5.07296 4.13296 4.99969 4.084 4.91904 4.0506C4.8384 4.01719 4.75197 4 4.66468 4C4.57739 4 4.49096 4.01719 4.41032 4.0506C4.32968 4.084 4.2564 4.13296 4.19468 4.19468C4.13296 4.2564 4.084 4.32968 4.0506 4.41032C4.01719 4.49096 4 4.57739 4 4.66468C4 4.75197 4.01719 4.8384 4.0506 4.91904C4.084 4.99969 4.13296 5.07296 4.19468 5.13468L7.45468 8.39468L4.19468 11.6547C4.13296 11.7164 4.084 11.7897 4.0506 11.8703C4.01719 11.951 4 12.0374 4 12.1247C4 12.212 4.01719 12.2984 4.0506 12.379C4.084 12.4597 4.13296 12.533 4.19468 12.5947C4.2564 12.6564 4.32968 12.7054 4.41032 12.7388C4.49096 12.7722 4.57739 12.7894 4.66468 12.7894C4.75197 12.7894 4.8384 12.7722 4.91904 12.7388C4.99969 12.7054 5.07296 12.6564 5.13468 12.5947L8.39468 9.33468L11.6547 12.5947C11.7164 12.6564 11.7897 12.7054 11.8703 12.7388C11.951 12.7722 12.0374 12.7894 12.1247 12.7894C12.212 12.7894 12.2984 12.7722 12.379 12.7388C12.4597 12.7054 12.533 12.6564 12.5947 12.5947C12.6564 12.533 12.7054 12.4597 12.7388 12.379C12.7722 12.2984 12.7894 12.212 12.7894 12.1247C12.7894 12.0374 12.7722 11.951 12.7388 11.8703C12.7054 11.7897 12.6564 11.7164 12.5947 11.6547L9.33468 8.39468L12.5947 5.13468C12.848 4.88135 12.848 4.45468 12.5947 4.20135Z" fill="#7E7E7E" />
<path className="fill-[var(--text-2)]" d="M12.5947 4.20135C12.533 4.13954 12.4597 4.09051 12.3791 4.05706C12.2984 4.0236 12.212 4.00638 12.1247 4.00638C12.0374 4.00638 11.9509 4.0236 11.8703 4.05706C11.7896 4.09051 11.7164 4.13954 11.6547 4.20135L8.39468 7.45468L5.13468 4.19468C5.07296 4.13296 4.99969 4.084 4.91904 4.0506C4.8384 4.01719 4.75197 4 4.66468 4C4.57739 4 4.49096 4.01719 4.41032 4.0506C4.32968 4.084 4.2564 4.13296 4.19468 4.19468C4.13296 4.2564 4.084 4.32968 4.0506 4.41032C4.01719 4.49096 4 4.57739 4 4.66468C4 4.75197 4.01719 4.8384 4.0506 4.91904C4.084 4.99969 4.13296 5.07296 4.19468 5.13468L7.45468 8.39468L4.19468 11.6547C4.13296 11.7164 4.084 11.7897 4.0506 11.8703C4.01719 11.951 4 12.0374 4 12.1247C4 12.212 4.01719 12.2984 4.0506 12.379C4.084 12.4597 4.13296 12.533 4.19468 12.5947C4.2564 12.6564 4.32968 12.7054 4.41032 12.7388C4.49096 12.7722 4.57739 12.7894 4.66468 12.7894C4.75197 12.7894 4.8384 12.7722 4.91904 12.7388C4.99969 12.7054 5.07296 12.6564 5.13468 12.5947L8.39468 9.33468L11.6547 12.5947C11.7164 12.6564 11.7897 12.7054 11.8703 12.7388C11.951 12.7722 12.0374 12.7894 12.1247 12.7894C12.212 12.7894 12.2984 12.7722 12.379 12.7388C12.4597 12.7054 12.533 12.6564 12.5947 12.5947C12.6564 12.533 12.7054 12.4597 12.7388 12.379C12.7722 12.2984 12.7894 12.212 12.7894 12.1247C12.7894 12.0374 12.7722 11.951 12.7388 11.8703C12.7054 11.7897 12.6564 11.7164 12.5947 11.6547L9.33468 8.39468L12.5947 5.13468C12.848 4.88135 12.848 4.45468 12.5947 4.20135Z" style={{ fill: 'var(--text-2)' }}/>
</svg>
);
}
/** «پذیرش کننده» — 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 (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none">
<path d="M12 12C14.7614 12 17 9.76142 17 7C17 4.23858 14.7614 2 12 2C9.23858 2 7 4.23858 7 7C7 9.76142 9.23858 12 12 12Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M3.41016 22C3.41016 18.13 7.26015 15 12.0002 15C12.9602 15 13.8902 15.13 14.7602 15.37" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M22 18C22 18.75 21.79 19.46 21.42 20.06C21.21 20.42 20.94 20.74 20.63 21C19.93 21.63 19.01 22 18 22C16.54 22 15.27 21.22 14.58 20.06C14.21 19.46 14 18.75 14 18C14 16.74 14.58 15.61 15.5 14.88C16.19 14.33 17.06 14 18 14C20.21 14 22 15.79 22 18Z" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M16.4395 18L17.4294 18.99L19.5594 17.02" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" style={{ color }}>
<path d="M12 12C14.7614 12 17 9.76142 17 7C17 4.23858 14.7614 2 12 2C9.23858 2 7 4.23858 7 7C7 9.76142 9.23858 12 12 12Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M3.41016 22C3.41016 18.13 7.26015 15 12.0002 15C12.9602 15 13.8902 15.13 14.7602 15.37" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M22 18C22 18.75 21.79 19.46 21.42 20.06C21.21 20.42 20.94 20.74 20.63 21C19.93 21.63 19.01 22 18 22C16.54 22 15.27 21.22 14.58 20.06C14.21 19.46 14 18.75 14 18C14 16.74 14.58 15.61 15.5 14.88C16.19 14.33 17.06 14 18 14C20.21 14 22 15.79 22 18Z" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M16.4395 18L17.4294 18.99L19.5594 17.02" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
/** آیکون «قیمت» کنار سرویس/کالا — tauri FilesServiceAddCard, verbatim. */
export function FilesServiceAddCard({ color = '#616161' }: { color?: string }) {
export function FilesServiceAddCard({ color = 'currentColor' }: { color?: string }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M1.66699 7.0835H11.2503" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5 13.75H6.66667" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.75 13.75H12.0833" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M18.3337 10.0248V13.4248C18.3337 16.3498 17.592 17.0832 14.6337 17.0832H5.36699C2.40866 17.0832 1.66699 16.3498 1.66699 13.4248V6.57484C1.66699 3.64984 2.40866 2.9165 5.36699 2.9165H11.2503" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M13.75 5.2085H18.3333" stroke={color} strokeWidth="1.5" strokeLinecap="round" />
<path d="M16.042 7.49984V2.9165" stroke={color} strokeWidth="1.5" strokeLinecap="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" style={{ color }}>
<path d="M1.66699 7.0835H11.2503" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5 13.75H6.66667" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.75 13.75H12.0833" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M18.3337 10.0248V13.4248C18.3337 16.3498 17.592 17.0832 14.6337 17.0832H5.36699C2.40866 17.0832 1.66699 16.3498 1.66699 13.4248V6.57484C1.66699 3.64984 2.40866 2.9165 5.36699 2.9165H11.2503" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M13.75 5.2085H18.3333" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<path d="M16.042 7.49984V2.9165" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
}
@@ -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 (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M22 22L20 20" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" style={{ color }}>
<path d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M22 22L20 20" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function TurnsFilter({ color = '#616161' }: { color?: string }) {
export function TurnsFilter({ color = 'currentColor' }: { color?: string }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 26 26" fill="none">
<path d="M11.191 17.9756H4.36523" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.2363 7.47539H21.0621" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path fillRule="evenodd" clipRule="evenodd" d="M9.45413 7.41677C9.45413 6.01315 8.30779 4.875 6.89406 4.875C5.48033 4.875 4.33398 6.01315 4.33398 7.41677C4.33398 8.8204 5.48033 9.95855 6.89406 9.95855C8.30779 9.95855 9.45413 8.8204 9.45413 7.41677Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path fillRule="evenodd" clipRule="evenodd" d="M21.667 17.9331C21.667 16.5295 20.5216 15.3914 19.1078 15.3914C17.6932 15.3914 16.5469 16.5295 16.5469 17.9331C16.5469 19.3368 17.6932 20.4749 19.1078 20.4749C20.5216 20.4749 21.667 19.3368 21.667 17.9331Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 26 26" fill="none" style={{ color }}>
<path d="M11.191 17.9756H4.36523" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14.2363 7.47539H21.0621" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path fillRule="evenodd" clipRule="evenodd" d="M9.45413 7.41677C9.45413 6.01315 8.30779 4.875 6.89406 4.875C5.48033 4.875 4.33398 6.01315 4.33398 7.41677C4.33398 8.8204 5.48033 9.95855 6.89406 9.95855C8.30779 9.95855 9.45413 8.8204 9.45413 7.41677Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path fillRule="evenodd" clipRule="evenodd" d="M21.667 17.9331C21.667 16.5295 20.5216 15.3914 19.1078 15.3914C17.6932 15.3914 16.5469 16.5295 16.5469 17.9331C16.5469 19.3368 17.6932 20.4749 19.1078 20.4749C20.5216 20.4749 21.667 19.3368 21.667 17.9331Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
/** 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 (
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 24 24" fill="none">
<path d="M9 22H15C20 22 22 20 22 15V9C22 4 20 2 15 2H9C4 2 2 4 2 9V15C2 20 4 22 9 22Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M2.0293 8.5H21.9993" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M2.0293 15.5H21.9993" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.50977 21.9898V2.00977" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M15.5098 21.9898V2.00977" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 24 24" fill="none" style={{ color }}>
<path d="M9 22H15C20 22 22 20 22 15V9C22 4 20 2 15 2H9C4 2 2 4 2 9V15C2 20 4 22 9 22Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M2.0293 8.5H21.9993" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M2.0293 15.5H21.9993" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.50977 21.9898V2.00977" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M15.5098 21.9898V2.00977" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
/** 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 (
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 24 24" fill="none">
<path d="M5 10H7C9 10 10 9 10 7V5C10 3 9 2 7 2H5C3 2 2 3 2 5V7C2 9 3 10 5 10Z" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M17 10H19C21 10 22 9 22 7V5C22 3 21 2 19 2H17C15 2 14 3 14 5V7C14 9 15 10 17 10Z" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M17 22H19C21 22 22 21 22 19V17C22 15 21 14 19 14H17C15 14 14 15 14 17V19C14 21 15 22 17 22Z" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5 22H7C9 22 10 21 10 19V17C10 15 9 14 7 14H5C3 14 2 15 2 17V19C2 21 3 22 5 22Z" stroke={color} strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 24 24" fill="none" style={{ color }}>
<path d="M5 10H7C9 10 10 9 10 7V5C10 3 9 2 7 2H5C3 2 2 3 2 5V7C2 9 3 10 5 10Z" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M17 10H19C21 10 22 9 22 7V5C22 3 21 2 19 2H17C15 2 14 3 14 5V7C14 9 15 10 17 10Z" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M17 22H19C21 22 22 21 22 19V17C22 15 21 14 19 14H17C15 14 14 15 14 17V19C14 21 15 22 17 22Z" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5 22H7C9 22 10 21 10 19V17C10 15 9 14 7 14H5C3 14 2 15 2 17V19C2 21 3 22 5 22Z" stroke="currentColor" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function AddTurn({ color = '#E1E1E1' }: { color?: string }) {
export function AddTurn({ color = 'currentColor' }: { color?: string }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M5 10H15" stroke={color} strokeWidth="1.5" strokeLinecap="round" />
<path d="M10 15V5" stroke={color} strokeWidth="1.5" strokeLinecap="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" style={{ color }}>
<path d="M5 10H15" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
<path d="M10 15V5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
}
@@ -101,7 +101,7 @@ export default function AddPackageModal({ open, editing, items, saving, onClose,
<div
style={{
flex: '1 1 300px', minHeight: 320, borderRadius: 8, padding: 24,
border: '1px dashed #636ed0', display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
border: '1px dashed var(--primary)', display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
}}
>
<div>
@@ -114,9 +114,9 @@ export default function AddPackageModal({ open, editing, items, saving, onClose,
<div key={idx} style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<span style={{ flex: 1, minWidth: 0, fontSize: 14, fontWeight: 600, color: 'var(--text-2)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{l.name}</span>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: 97, height: 40, borderRadius: 4, border: '1px solid var(--border-2)', padding: '0 8px' }}>
<button type="button" onClick={() => changeAmount(idx, 1)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#f0753b', fontSize: 18, fontWeight: 600 }}>+</button>
<span style={{ color: '#f0753b', fontSize: 14, fontWeight: 500 }}>{l.amount}</span>
<button type="button" onClick={() => changeAmount(idx, -1)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#f0753b', fontSize: 18, fontWeight: 600 }}>-</button>
<button type="button" onClick={() => changeAmount(idx, 1)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--accent)', fontSize: 18, fontWeight: 600 }}>+</button>
<span style={{ color: 'var(--accent)', fontSize: 14, fontWeight: 500 }}>{l.amount}</span>
<button type="button" onClick={() => changeAmount(idx, -1)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--accent)', fontSize: 18, fontWeight: 600 }}>-</button>
</div>
<span style={{ fontSize: 14, color: 'var(--text-2)', whiteSpace: 'nowrap' }}>{formatRial(l.price)}</span>
<button type="button" onClick={() => removeLine(idx)} aria-label="حذف" style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--danger)' }}>
@@ -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,
}}
>
<EllipsisHorizontalCircleIcon style={{ width: 18, height: 18 }} />
@@ -24,7 +24,7 @@ export default function InventoryItemsTable({ items, onEdit, onDelete }: Props)
{/* Desktop table */}
<div
className="inv-desktop"
style={{ border: '1px solid #E7E7E7', borderRadius: 8, overflow: 'hidden' }}
style={{ border: '1px solid var(--border)', borderRadius: 8, overflow: 'hidden' }}
>
<table className="inv-table">
<thead>
@@ -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 = <div style={{ height: 1, background: '#d7d7d7', margin: '8px 0' }} className="inv-divider" />;
const divider = <div style={{ height: 1, background: 'var(--border-2)', margin: '8px 0' }} className="inv-divider" />;
return (
<div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 8, padding: 16 }}>
{/* Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ width: 20, height: 20, background: '#f0753b', borderRadius: '50%', flexShrink: 0 }} />
<span style={{ width: 20, height: 20, background: 'var(--accent)', borderRadius: '50%', flexShrink: 0 }} />
<span style={{ fontWeight: 500, fontSize: 16, color: 'var(--text)' }}>{pkg.title}</span>
</div>
<span
style={{
width: 122, height: 34, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
borderRadius: 4, fontWeight: 600, fontSize: 16,
color: pkg.available ? '#3f9954' : '#d32f2f',
background: pkg.available ? '#f0faf2' : '#fdebed',
color: pkg.available ? 'var(--success)' : 'var(--danger)',
background: pkg.available ? 'var(--success-bg)' : 'var(--danger-bg)',
}}
>
{pkg.available ? 'موجودی کافی' : 'موجودی ناکافی'}
@@ -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',
};
+1 -1
View File
@@ -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 && <CheckIcon style={{ width: 16, height: 16 }} />}
@@ -169,30 +169,30 @@ function PersianDateInput({ value, onChange, minDate, placeholder }: {
<div>
<button ref={btnRef} type="button" onClick={openPicker}
className="cp-input h-11 flex items-center justify-between w-full cursor-pointer">
<span className={value ? 'text-slate-800 dark:text-slate-200' : 'text-slate-400 dark:text-slate-500'}>
<span className={value ? 'text-[var(--text)] dark:text-[var(--text)]' : 'text-[var(--text-3)] dark:text-[var(--text-2)]'}>
{value ? formatPersianDate(value) : (placeholder ?? 'انتخاب تاریخ')}
</span>
<CalendarIcon className="w-4 h-4 text-slate-400 shrink-0 mr-2" />
<CalendarIcon className="w-4 h-4 text-[var(--text-3)] shrink-0 mr-2" />
</button>
{open && createPortal(
<div ref={dropRef} style={dropStyle}
className="bg-white dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-2xl shadow-2xl overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-100 dark:border-gray-800">
className="bg-[var(--surface)] border border-[var(--border)] rounded-2xl shadow-2xl overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)]">
<button type="button" onClick={nextMonth}
className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-slate-100 dark:hover:bg-gray-800 text-slate-500 transition-colors">
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">
<ChevronRightIcon className="w-4 h-4" />
</button>
<span className="text-sm font-semibold text-slate-800 dark:text-slate-100" dir="rtl">
<span className="text-sm font-semibold text-[var(--text)]" dir="rtl">
{viewJm > 0 ? JALALI_MONTHS[viewJm - 1] : ''} {toPersianNums(viewJy)}
</span>
<button type="button" onClick={prevMonth}
className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-slate-100 dark:hover:bg-gray-800 text-slate-500 transition-colors">
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">
<ChevronLeftIcon className="w-4 h-4" />
</button>
</div>
<div className="grid grid-cols-7 px-3 pt-2 pb-1">
{JALALI_DAYS_SHORT.map(d => (
<div key={d} className="text-center text-xs font-medium text-slate-400 dark:text-slate-500 py-1">{d}</div>
<div key={d} className="text-center text-xs font-medium text-[var(--text-3)] py-1">{d}</div>
))}
</div>
<div className="grid grid-cols-7 gap-0.5 px-3 pb-3">
@@ -205,10 +205,10 @@ function PersianDateInput({ value, onChange, minDate, placeholder }: {
return (
<button key={jd} type="button" disabled={dis} onClick={() => 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)}
</button>
);
@@ -331,10 +331,10 @@ function SlotEditor({ slots, onChange }: {
<div className="space-y-2">
{slots.map((slot, i) => (
<div key={i} className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-slate-500 dark:text-slate-400 shrink-0">از</span>
<span className="text-xs text-[var(--text-2)] shrink-0">از</span>
<input type="time" value={slot.start} onChange={e => update(i, 'start', e.target.value)}
className="cp-input h-8 w-28 text-sm font-mono text-center px-2" />
<span className="text-xs text-slate-500 dark:text-slate-400 shrink-0">تا</span>
<span className="text-xs text-[var(--text-2)] shrink-0">تا</span>
<input type="time" value={slot.end} onChange={e => update(i, 'end', e.target.value)}
className="cp-input h-8 w-28 text-sm font-mono text-center px-2" />
<div style={{ width: 110 }}>
@@ -346,7 +346,7 @@ function SlotEditor({ slots, onChange }: {
/>
</div>
<button type="button" onClick={() => 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">
<XMarkIcon className="w-3.5 h-3.5" />
</button>
</div>
@@ -441,7 +441,7 @@ function SessionEditor({ session, onChange, onRemove, addresses, serviceMode = f
}}>
<span style={{
position: 'absolute', top: 3, borderRadius: '50%',
width: 16, height: 16, background: '#fff',
width: 16, height: 16, background: 'var(--surface)',
boxShadow: '0 1px 3px rgba(0,0,0,.25)',
insetInlineStart: session.has_rest ? 20 : 3,
}} />
@@ -511,12 +511,12 @@ function NoLocationsNotice({ clinicUuid }: { clinicUuid?: string | null }) {
: ['ابتدا آدرس مطب را ثبت کنید', 'برنامه کاری نیاز به حداقل یک مکان نوبت دارد'];
return (
<div className="flex flex-col items-center justify-center gap-3 py-12 text-center">
<div className="w-12 h-12 rounded-full bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/30 flex items-center justify-center">
<MapPinIcon className="w-6 h-6 text-amber-500" />
<div className="w-12 h-12 rounded-full bg-[var(--warning-bg)] dark:bg-amber-500/10 border border-[var(--warning)] dark:border-amber-500/30 flex items-center justify-center">
<MapPinIcon className="w-6 h-6 text-[var(--warning)]" />
</div>
<div>
<p className="text-sm font-medium text-slate-700 dark:text-slate-200">{title}</p>
<p className="text-xs text-slate-400 dark:text-slate-500 mt-1">{hint}</p>
<p className="text-sm font-medium text-[var(--text)]">{title}</p>
<p className="text-xs text-[var(--text-3)] mt-1">{hint}</p>
</div>
</div>
);
@@ -627,8 +627,8 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
return (
<div className="space-y-2">
{totalSlots > 0 && (
<div className="flex items-center gap-2 mb-3 p-3 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/30">
<CalendarIcon className="w-4 h-4 text-emerald-600 dark:text-emerald-400 shrink-0" />
<div className="flex items-center gap-2 mb-3 p-3 rounded-xl bg-[var(--success-bg)] dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/30">
<CalendarIcon className="w-4 h-4 text-[var(--success)] dark:text-emerald-400 shrink-0" />
<span className="text-sm text-emerald-700 dark:text-emerald-300">
مجموع <strong>{totalSlots}</strong> نوبت در هفته
</span>
@@ -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 (
<div key={day.key} className="flex items-center gap-3 px-4 py-3 rounded-xl border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800/40">
<div key={day.key} className="flex items-center gap-3 px-4 py-3 rounded-xl border border-[var(--border)] bg-[var(--surface)] dark:bg-[var(--surface)]/40">
<b style={{ fontSize: 14, minWidth: 56, flexShrink: 0 }}>{day.label}</b>
<div className="flex-1 flex gap-2 items-center flex-wrap min-w-0">
{activeSessions.length > 0 ? activeSessions.map((s, i) => (
<span key={i} className="text-xs px-2 py-1 rounded-lg bg-slate-100 dark:bg-gray-700 text-slate-700 dark:text-slate-200" style={{ fontFamily: 'monospace', direction: 'ltr', whiteSpace: 'nowrap' }}>
<span key={i} className="text-xs px-2 py-1 rounded-lg bg-[var(--surface-2)] text-[var(--text)]" style={{ fontFamily: 'monospace', direction: 'ltr', whiteSpace: 'nowrap' }}>
{s.start_time} {s.end_time}
</span>
)) : (
@@ -665,8 +665,8 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
<div className="space-y-2">
{/* ─ خلاصه کل هفته */}
{totalSlots > 0 && (
<div className="flex items-center gap-2 mb-3 p-3 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/30">
<CalendarIcon className="w-4 h-4 text-emerald-600 dark:text-emerald-400 shrink-0" />
<div className="flex items-center gap-2 mb-3 p-3 rounded-xl bg-[var(--success-bg)] dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/30">
<CalendarIcon className="w-4 h-4 text-[var(--success)] dark:text-emerald-400 shrink-0" />
<span className="text-sm text-emerald-700 dark:text-emerald-300">
مجموع <strong>{totalSlots}</strong> نوبت در هفته
</span>
@@ -674,9 +674,9 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
)}
{/* ─ روش نوبت‌دهی */}
<div className="mb-3 rounded-xl border border-slate-200 dark:border-gray-700 overflow-hidden">
<div className="px-4 py-3 bg-slate-50 dark:bg-gray-800/50">
<span className="text-sm font-medium text-slate-700 dark:text-slate-200">روش نوبتدهی</span>
<div className="mb-3 rounded-xl border border-[var(--border)] overflow-hidden">
<div className="px-4 py-3 bg-[var(--surface-2)] dark:bg-[var(--surface)]/50">
<span className="text-sm font-medium text-[var(--text)]">روش نوبتدهی</span>
</div>
<div className="px-4 py-3 space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
@@ -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)]'}`}
>
<div className="flex items-center gap-2 mb-1">
<span className={`w-3.5 h-3.5 rounded-full border shrink-0 ${selected ? 'border-[var(--primary)] bg-[var(--primary)]' : 'border-slate-300 dark:border-gray-600'}`} />
<span className="text-sm font-medium text-slate-700 dark:text-slate-200">{lbl}</span>
<span className={`w-3.5 h-3.5 rounded-full border shrink-0 ${selected ? 'border-[var(--primary)] bg-[var(--primary)]' : 'border-[var(--border-2)] dark:border-gray-600'}`} />
<span className="text-sm font-medium text-[var(--text)]">{lbl}</span>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 leading-relaxed">{desc}</p>
<p className="text-xs text-[var(--text-2)] leading-relaxed">{desc}</p>
</button>
);
})}
</div>
{modeLocked ? (
<p className="text-xs text-slate-500 dark:text-slate-400 flex items-center gap-1.5">
<p className="text-xs text-[var(--text-2)] flex items-center gap-1.5">
<LockClosedIcon className="w-3.5 h-3.5 shrink-0" />
نوع نوبتدهی ثبت شده و دیگر قابل تغییر نیست.
</p>
) : (
<p className="text-xs text-amber-600 dark:text-amber-400 leading-relaxed">
<p className="text-xs text-[var(--warning)] dark:text-amber-400 leading-relaxed">
توجه: نوع نوبتدهی پس از اولین ثبت <span className="font-medium">بههیچعنوان قابل تغییر نیست</span>. پیش از ذخیره با دقت انتخاب کنید.
</p>
)}
{meta.booking_mode === 'service' ? (
<>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-slate-600 dark:text-slate-400">فاصله بین نوبتها</span>
<span className="text-sm text-[var(--text-2)]">فاصله بین نوبتها</span>
<input
type="text"
inputMode="numeric"
dir="ltr"
value={meta.buffer_minutes}
onChange={(e) => 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"
/>
<span className="text-sm text-slate-600 dark:text-slate-400">دقیقه</span>
<span className="text-sm text-[var(--text-2)]">دقیقه</span>
</div>
<p className="text-xs text-slate-400 dark:text-slate-500 leading-relaxed">
<p className="text-xs text-[var(--text-3)] leading-relaxed">
مدت هر نوبت از «مدت سرویس» انتخابشده تعیین میشود. لازم است حداقل یک سرویس با «نمایش در نوبتدهی» در بخش <span className="font-medium">{clinicUuid ? 'سرویس‌های کلینیک' : 'سرویس‌ها'}</span> تعریف کنید، وگرنه ذخیره نمیشود.
</p>
</>
) : (
<p className="text-xs text-slate-400 dark:text-slate-500 leading-relaxed">
<p className="text-xs text-[var(--text-3)] leading-relaxed">
مدت هر نوبت از «زمان هر نوبت» در شیفتهای زیر تعیین میشود.
</p>
)}
@@ -743,12 +743,12 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
</div>
{/* ─ نوبت‌دهی آنلاین */}
<div className="mb-3 rounded-xl border border-slate-200 dark:border-gray-700 overflow-hidden">
<div className="mb-3 rounded-xl border border-[var(--border)] overflow-hidden">
{/* header + toggle */}
<label className="flex items-center justify-between gap-3 px-4 py-3 cursor-pointer bg-slate-50 dark:bg-gray-800/50">
<label className="flex items-center justify-between gap-3 px-4 py-3 cursor-pointer bg-[var(--surface-2)] dark:bg-[var(--surface)]/50">
<div className="flex items-center gap-2">
<GlobeAltIcon className="w-4 h-4 text-slate-500 dark:text-slate-400 shrink-0" />
<span className="text-sm font-medium text-slate-700 dark:text-slate-200">نوبتدهی آنلاین</span>
<GlobeAltIcon className="w-4 h-4 text-[var(--text-2)] shrink-0" />
<span className="text-sm font-medium text-[var(--text)]">نوبتدهی آنلاین</span>
</div>
<span className="relative inline-flex shrink-0">
<input
@@ -758,14 +758,14 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
onChange={(e) => setMeta(m => ({ ...m, online_booking_enabled: e.target.checked }))}
/>
<span className="w-10 h-6 rounded-full bg-slate-300 dark:bg-gray-600 transition-colors peer-checked:bg-emerald-500" />
<span className="absolute top-0.5 right-0.5 w-5 h-5 rounded-full bg-white shadow transition-transform peer-checked:-translate-x-4" />
<span className="absolute top-0.5 right-0.5 w-5 h-5 rounded-full bg-[var(--surface)] shadow transition-transform peer-checked:-translate-x-4" />
</span>
</label>
{/* booking window control */}
<div className={`px-4 py-3 transition-opacity ${meta.online_booking_enabled ? '' : 'opacity-50 pointer-events-none'}`}>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-slate-600 dark:text-slate-400">رزرو آنلاین تا</span>
<span className="text-sm text-[var(--text-2)]">رزرو آنلاین تا</span>
<div className="flex items-stretch gap-2">
<input
type="text"
@@ -774,7 +774,7 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
value={meta.booking_window_value}
disabled={!meta.online_booking_enabled}
onChange={(e) => setMeta(m => ({ ...m, booking_window_value: Math.max(1, Number(digitsOnly(e.target.value)) || 1) }))}
className="w-14 text-center text-sm rounded-lg border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-900 focus:outline-none focus:ring-0 px-2 py-1.5"
className="w-14 text-center text-sm rounded-lg border border-[var(--border)] bg-[var(--surface)] focus:outline-none focus:ring-0 px-2 py-1.5"
/>
<div style={{ width: 110 }}>
<GlobalSearchableSelect
@@ -786,9 +786,9 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
/>
</div>
</div>
<span className="text-sm text-slate-600 dark:text-slate-400">آینده</span>
<span className="text-sm text-[var(--text-2)]">آینده</span>
</div>
<p className="text-xs text-slate-400 dark:text-slate-500 mt-2 leading-relaxed">
<p className="text-xs text-[var(--text-3)] mt-2 leading-relaxed">
بیمار فقط تا این بازه میتواند آنلاین نوبت بگیرد؛ روزهای بعد از آن روی تقویم غیرفعالاند.
</p>
</div>
@@ -864,7 +864,7 @@ export function WeeklyScheduleTab({ doctorUuid, clinicUuid, addresses, readOnly
<div className="flex items-center justify-between pt-3">
{(hasAnyOverlap || missingLocation) && (
<p className="text-xs text-red-500 dark:text-red-400 flex items-center gap-1">
<p className="text-xs text-[var(--danger)] dark:text-red-400 flex items-center gap-1">
<ExclamationTriangleIcon className="w-3.5 h-3.5" />
{hasAnyOverlap ? 'تداخل زمانی در برنامه وجود دارد' : 'مکان مطب برای همه بازه‌ها الزامی است'}
</p>
@@ -956,17 +956,17 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, clinicUuid, on
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">تاریخ</label>
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">تاریخ</label>
<PersianDateInput value={dateStr} onChange={setDateStr} placeholder="انتخاب تاریخ" />
</div>
{/* ─ نوع override */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">نوع این روز</label>
<label className="block text-sm font-medium text-[var(--text)] mb-2">نوع این روز</label>
<div className="grid grid-cols-2 gap-2">
<button type="button"
onClick={() => 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)]'}`}>
<XCircleIcon className="w-5 h-5 shrink-0" />
<div>
<p className="font-medium leading-tight">تعطیل است</p>
@@ -975,7 +975,7 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, clinicUuid, on
</button>
<button type="button"
onClick={() => 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)]'}`}>
<CalendarIcon className="w-5 h-5 shrink-0" />
<div>
<p className="font-medium leading-tight">ساعات خاص</p>
@@ -986,7 +986,7 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, clinicUuid, on
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">دلیل (اختیاری)</label>
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">دلیل (اختیاری)</label>
<input type="text" value={reason} onChange={e => setReason(e.target.value)}
placeholder={overrideType === 'closed' ? 'مثال: سفر، مریضی، کنگره...' : 'مثال: شیفت اضطراری، ویزیت خاص...'}
className="input" />
@@ -1020,9 +1020,9 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, clinicUuid, on
)}
{overrideType === 'closed' && (
<div className="flex items-start gap-2 p-3 rounded-xl bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/30">
<ExclamationTriangleIcon className="w-4 h-4 text-red-500 shrink-0 mt-0.5" />
<p className="text-xs text-red-700 dark:text-red-400">
<div className="flex items-start gap-2 p-3 rounded-xl bg-[var(--danger-bg)] dark:bg-[var(--danger)]/10 border border-[var(--danger)] dark:border-red-500/30">
<ExclamationTriangleIcon className="w-4 h-4 text-[var(--danger)] shrink-0 mt-0.5" />
<p className="text-xs text-[var(--danger)] dark:text-red-400">
در این روز هیچ نوبتی داده نخواهد شد، حتی اگر در برنامه هفتگی ساعات کاری داشته باشید.
</p>
</div>
@@ -1072,37 +1072,37 @@ function DateOverridesTab({ doctorUuid, clinicUuid, addresses, readOnly = false
</div>
)}
{overrides.length === 0 ? (
<div className="text-center py-10 text-slate-400 dark:text-slate-500">
<div className="text-center py-10 text-[var(--text-3)]">
<CalendarIcon className="w-10 h-10 mx-auto mb-2 opacity-30" />
<p className="text-sm">هیچ تاریخ خاصی تنظیم نشده</p>
</div>
) : (
<div className="space-y-2">
{overrides.map(ov => (
<div key={ov.uuid} className="flex items-center gap-3 p-3.5 rounded-xl border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800/60">
<div key={ov.uuid} className="flex items-center gap-3 p-3.5 rounded-xl border border-[var(--border)] bg-[var(--surface)] dark:bg-[var(--surface)]/60">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-slate-800 dark:text-slate-200">{formatPersianDate(tsToDate(ov.date))}</span>
<span className="text-sm font-medium text-[var(--text)]">{formatPersianDate(tsToDate(ov.date))}</span>
{ov.active ? (
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-300">
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--warning-bg)] dark:bg-amber-500/20 text-amber-700 dark:text-amber-300">
{ov.custom_slots.length > 0 ? `${ov.custom_slots.length} بازه سفارشی` : 'ساعات خاص'}
</span>
) : (
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-red-100 dark:bg-red-500/20 text-red-600 dark:text-red-400">
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--danger-bg)] dark:bg-[var(--danger)]/20 text-[var(--danger)] dark:text-red-400">
تعطیل
</span>
)}
</div>
{ov.reason && <p className="text-xs text-slate-400 dark:text-slate-500 mt-0.5">{ov.reason}</p>}
{ov.reason && <p className="text-xs text-[var(--text-3)] mt-0.5">{ov.reason}</p>}
</div>
{!readOnly && (
<div className="flex items-center gap-1 shrink-0">
<button onClick={() => { 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">
<PencilIcon className="w-3.5 h-3.5" />
</button>
<button onClick={() => 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">
<TrashIcon className="w-3.5 h-3.5" />
</button>
</div>
@@ -1173,19 +1173,19 @@ function HolidayModal({ open, onClose, existing, doctorUuid, clinicUuid, onSaved
<div className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">از تاریخ</label>
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">از تاریخ</label>
<PersianDateInput value={startDate} onChange={v => { setStartDate(v); if (endDate && v > endDate) setEndDate(v); }} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">تا تاریخ</label>
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">تا تاریخ</label>
<PersianDateInput value={endDate} onChange={setEndDate} minDate={startDate} />
</div>
</div>
{endDate && startDate && endDate < startDate && (
<p className="text-xs text-red-500">تاریخ پایان نمیتواند قبل از تاریخ شروع باشد</p>
<p className="text-xs text-[var(--danger)]">تاریخ پایان نمیتواند قبل از تاریخ شروع باشد</p>
)}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">دلیل (اختیاری)</label>
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">دلیل (اختیاری)</label>
<input type="text" value={reason} onChange={e => setReason(e.target.value)}
placeholder="مثال: سفر، کنگره پزشکی..." className="input" />
</div>
@@ -1238,7 +1238,7 @@ function HolidaysTab({ doctorUuid, clinicUuid, readOnly = false }: { doctorUuid:
</div>
)}
{holidays.length === 0 ? (
<div className="text-center py-10 text-slate-400 dark:text-slate-500">
<div className="text-center py-10 text-[var(--text-3)]">
<CalendarIcon className="w-10 h-10 mx-auto mb-2 opacity-30" />
<p className="text-sm">هیچ تعطیلاتی ثبت نشده</p>
</div>
@@ -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 (
<div key={h.uuid} className={`flex items-center gap-3 p-3.5 rounded-xl border transition-colors ${isCurrent ? 'border-red-200 dark:border-red-500/30 bg-red-50/40 dark:bg-red-500/5' : isPast ? 'border-slate-200 dark:border-gray-700 bg-slate-50/40 dark:bg-gray-800/30 opacity-70' : 'border-orange-200 dark:border-orange-500/30 bg-orange-50/30 dark:bg-orange-500/5'}`}>
<div key={h.uuid} className={`flex items-center gap-3 p-3.5 rounded-xl border transition-colors ${isCurrent ? 'border-[var(--danger)] dark:border-red-500/30 bg-[var(--danger-bg)]/40 dark:bg-[var(--danger)]/5' : isPast ? 'border-[var(--border)] dark:border-[var(--border)] bg-[var(--surface-2)]/40 dark:bg-[var(--surface)]/30 opacity-70' : 'border-orange-200 dark:border-orange-500/30 bg-orange-50/30 dark:bg-orange-500/5'}`}>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className={`text-sm font-medium ${isPast ? 'text-slate-500 dark:text-slate-400' : 'text-slate-800 dark:text-slate-200'}`}>
<span className={`text-sm font-medium ${isPast ? 'text-[var(--text-2)] dark:text-[var(--text-3)]' : 'text-[var(--text)] dark:text-[var(--text)]'}`}>
{sameDay ? formatPersianDate(tsToDate(h.start_date)) : `${formatPersianDate(tsToDate(h.start_date))} تا ${formatPersianDate(tsToDate(h.end_date))}`}
</span>
{isCurrent && h.active && (
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-red-100 dark:bg-red-500/20 text-red-600 dark:text-red-400 animate-pulse">در جریان</span>
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-[var(--danger-bg)] dark:bg-[var(--danger)]/20 text-[var(--danger)] dark:text-red-400 animate-pulse">در جریان</span>
)}
{!isCurrent && !isPast && h.active && (
<span className="text-xs px-2 py-0.5 rounded-full font-medium bg-orange-100 dark:bg-orange-500/20 text-orange-600 dark:text-orange-400">آینده</span>
)}
{isPast && (
<span className="text-xs px-2 py-0.5 rounded-full bg-slate-100 dark:bg-gray-700 text-slate-400 dark:text-slate-500">گذشته</span>
<span className="text-xs px-2 py-0.5 rounded-full bg-[var(--surface-2)] text-[var(--text-3)]">گذشته</span>
)}
{!h.active && (
<span className="text-xs px-2 py-0.5 rounded-full bg-slate-100 dark:bg-gray-700 text-slate-400 dark:text-slate-500">غیرفعال</span>
<span className="text-xs px-2 py-0.5 rounded-full bg-[var(--surface-2)] text-[var(--text-3)]">غیرفعال</span>
)}
</div>
{h.reason && <p className="text-xs text-slate-400 dark:text-slate-500 mt-0.5">{h.reason}</p>}
{h.reason && <p className="text-xs text-[var(--text-3)] mt-0.5">{h.reason}</p>}
</div>
{!readOnly && (
<div className="flex items-center gap-1 shrink-0">
<button onClick={() => 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 ? 'غیرفعال' : 'فعال'}
</button>
<button onClick={() => { 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">
<PencilIcon className="w-3.5 h-3.5" />
</button>
<button onClick={() => 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">
<TrashIcon className="w-3.5 h-3.5" />
</button>
</div>
@@ -1323,14 +1323,14 @@ export function ScheduleSection({ doctorUuid, clinicUuid, readOnly = false }: {
return (
<div className="cp-card p-6">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-4">برنامه کاری</h2>
<div className="flex gap-1 mb-5 bg-slate-100 dark:bg-gray-800 p-1 rounded-xl">
<h2 className="text-sm font-semibold text-[var(--text)] mb-4">برنامه کاری</h2>
<div className="flex gap-1 mb-5 bg-[var(--surface-2)] p-1 rounded-xl">
{SCHEDULE_TABS.map(t => (
<button key={t.id} type="button" onClick={() => 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}
</button>
+33 -33
View File
@@ -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) => (
<div className="dark:border-[#404040]" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 74, height: 27, border: '1px solid #d1d1d1', borderRadius: 8, padding: '0 4px', gap: 4 }}>
<button type="button" aria-label="افزایش" onClick={() => set(qty + 1)} style={{ border: 'none', background: 'transparent', color: '#FF7A45', cursor: 'pointer', display: 'flex' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 74, height: 27, border: '1px solid var(--border-2)', borderRadius: 8, padding: '0 4px', gap: 4 }}>
<button type="button" aria-label="افزایش" onClick={() => set(qty + 1)} style={{ border: 'none', background: 'transparent', color: 'var(--accent)', cursor: 'pointer', display: 'flex' }}>
<PlusIcon style={{ width: 16 }} />
</button>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, color: '#525252', minWidth: 20, textAlign: 'center' }}>{qty}</span>
<button type="button" aria-label="کاهش" onClick={() => set(qty - 1)} style={{ border: 'none', background: 'transparent', color: '#FF7A45', cursor: 'pointer', display: 'flex' }}>
{qty > 1 ? <MinusIcon style={{ width: 16 }} /> : <TrashRed size={16} color="#f17732" />}
<span style={{ fontSize: 13, color: 'var(--text)', minWidth: 20, textAlign: 'center' }}>{qty}</span>
<button type="button" aria-label="کاهش" onClick={() => set(qty - 1)} style={{ border: 'none', background: 'transparent', color: 'var(--accent)', cursor: 'pointer', display: 'flex' }}>
{qty > 1 ? <MinusIcon style={{ width: 16 }} /> : <TrashRed size={16} color="var(--accent)" />}
</button>
</div>
);
@@ -353,8 +353,8 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel, e
<div style={{ marginTop: 16 }}>
{/* پذیرش کننده — tauri UserTick + کاربر لاگین‌شده */}
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 28 }}>
<UserTick color="#6B7280" size={18} />
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, color: '#616161' }}>پذیرش کننده: {userName || '—'}</span>
<UserTick color="var(--text-2)" size={18} />
<span style={{ fontSize: 14, color: 'var(--text-2)' }}>پذیرش کننده: {userName || '—'}</span>
</div>
{/* تاریخ و ساعت پذیرش */}
@@ -365,8 +365,8 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel, e
</div>
<div>
<span style={fieldLabel}>ساعت پذیرش</span>
<div className="bg-white dark:bg-[#222433] dark:border-[#404040]" style={{ display: 'flex', alignItems: 'center', gap: 8, height: 48, border: '1px solid #e1e1e1', borderRadius: 4, padding: '0 8px' }}>
<ClockP color="#6B7280" size={18} />
<div className="bg-[var(--surface)]" style={{ display: 'flex', alignItems: 'center', gap: 8, height: 48, border: '1px solid var(--border-2)', borderRadius: 4, padding: '0 8px' }}>
<ClockP color="var(--text-2)" size={18} />
<input
type="time"
className="time-input-plain"
@@ -392,28 +392,28 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel, e
<SearchableSelect inputId="service-select" options={itemOptions} value={itemUuid} onChange={v => setItemUuid(v ? String(v) : '')} placeholder="جستجو و انتخاب سرویس..." />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 20, flexShrink: 0 }}>
<FilesServiceAddCard color="#6B7280" />
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, color: '#3b3b3b' }}>
<FilesServiceAddCard color="var(--text-2)" />
<span style={{ fontSize: 13, color: 'var(--text)' }}>
قیمت: {currentItem ? formatRial(currentItem.price_rials) : '—'}
</span>
</div>
<button type="button" aria-label="افزودن سرویس" onClick={addService} disabled={!itemUuid} style={{ minWidth: 40, height: 40, borderRadius: 8, background: '#5559CE', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', marginTop: 20 }}>
<PlusIcon style={{ width: 18, color: '#fff' }} />
<button type="button" aria-label="افزودن سرویس" onClick={addService} disabled={!itemUuid} style={{ minWidth: 40, height: 40, borderRadius: 8, background: 'var(--primary)', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', marginTop: 20 }}>
<PlusIcon style={{ width: 18, color: 'var(--on-primary)' }} />
</button>
</div>
{/* خدمات انتخاب شده */}
{selectedServices.length > 0 && (
<div style={{ border: '1px dashed #C7C9F4', borderRadius: 8, padding: 16, marginBottom: 16 }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, fontWeight: 600, color: '#525252', display: 'block', marginBottom: 16 }}>خدمات انتخاب شده</span>
<div style={{ border: '1px dashed var(--primary-soft2)', borderRadius: 8, padding: 16, marginBottom: 16 }}>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)', display: 'block', marginBottom: 16 }}>خدمات انتخاب شده</span>
{selectedServices.map(item => (
<div key={item.uuid} style={{ display: 'flex', alignItems: 'center', gap: 24, padding: '8px 0' }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, color: '#525252', minWidth: 120 }}>{item.name}</span>
<span style={{ fontSize: 14, color: 'var(--text)', minWidth: 120 }}>{item.name}</span>
{counter(item.qty, (q) => setServiceQty(item.uuid, q))}
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 12, fontWeight: 500, color: '#3b3b3b' }}>قیمت: {formatRial(item.price * item.qty)}</span>
<span style={{ fontSize: 12, fontWeight: 500, color: 'var(--text)' }}>قیمت: {formatRial(item.price * item.qty)}</span>
</div>
))}
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, fontWeight: 600, color: '#525252', display: 'block', marginTop: 16 }}>مجموع قیمت خدمات: {formatRial(servicesTotal)}</span>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)', display: 'block', marginTop: 16 }}>مجموع قیمت خدمات: {formatRial(servicesTotal)}</span>
</div>
)}
@@ -425,36 +425,36 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel, e
{/* انتخاب کالای مصرفی */}
<div style={{ marginBottom: 16 }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, color: '#525252', display: 'block', marginBottom: 8 }}>انتخاب کالای مصرفی</span>
<span style={{ fontSize: 14, color: 'var(--text)', display: 'block', marginBottom: 8 }}>انتخاب کالای مصرفی</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ width: 313, maxWidth: '60%' }}>
<SearchableSelect inputId="consumable-select" options={consumableOptions} value={consumableUuid} onChange={v => setConsumableUuid(v ? String(v) : '')} placeholder="انتخاب کنید..." />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, flexShrink: 0 }}>
<FilesServiceAddCard color="#6B7280" />
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 13, color: '#6B7280' }}>
<FilesServiceAddCard color="var(--text-2)" />
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>
قیمت: {currentConsumable ? formatRial(currentConsumable.price) : '—'}
</span>
</div>
<button type="button" aria-label="افزودن کالا" onClick={addConsumable} disabled={!consumableUuid} style={{ minWidth: 40, height: 40, borderRadius: 8, background: '#5559CE', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<PlusIcon style={{ width: 18, color: '#fff' }} />
<button type="button" aria-label="افزودن کالا" onClick={addConsumable} disabled={!consumableUuid} style={{ minWidth: 40, height: 40, borderRadius: 8, background: 'var(--primary)', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<PlusIcon style={{ width: 18, color: 'var(--on-primary)' }} />
</button>
</div>
</div>
{/* کالاهای انتخاب شده — باکس خط‌چین tauri */}
<div style={{ border: '1px dashed #C7C9F4', borderRadius: 8, padding: 16, marginBottom: 16 }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, fontWeight: 600, color: '#525252', display: 'block', marginBottom: 16 }}>کالاهای انتخاب شده</span>
<div style={{ border: '1px dashed var(--primary-soft2)', borderRadius: 8, padding: 16, marginBottom: 16 }}>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)', display: 'block', marginBottom: 16 }}>کالاهای انتخاب شده</span>
{selectedConsumables.length === 0 ? (
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 13, color: '#6B7280' }}>کالایی انتخاب نشده است</span>
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>کالایی انتخاب نشده است</span>
) : selectedConsumables.map(item => (
<div key={item.uuid} style={{ display: 'flex', alignItems: 'center', gap: 24, padding: '8px 0' }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, color: '#525252', minWidth: 120 }}>{item.name}</span>
<span style={{ fontSize: 14, color: 'var(--text)', minWidth: 120 }}>{item.name}</span>
{counter(item.qty, (q) => setConsumableQty(item.uuid, q))}
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 12, fontWeight: 500, color: '#3b3b3b' }}>قیمت: {formatRial(item.price * item.qty)}</span>
<span style={{ fontSize: 12, fontWeight: 500, color: 'var(--text)' }}>قیمت: {formatRial(item.price * item.qty)}</span>
</div>
))}
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, fontWeight: 600, color: '#525252', display: 'block', marginTop: 16 }}>مجموع قیمت کالاها: {formatRial(consumablesTotal)}</span>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)', display: 'block', marginTop: 16 }}>مجموع قیمت کالاها: {formatRial(consumablesTotal)}</span>
</div>
{/* انتخاب پکیج */}
@@ -527,7 +527,7 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel, e
</div>
{/* خلاصه مبلغ */}
<div className="dark:border-[#404040]" style={{ borderTop: '1px solid var(--border)', paddingTop: 12, marginBottom: 16, display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ borderTop: '1px solid var(--border)', paddingTop: 12, marginBottom: 16, display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, color: 'var(--text-3)' }}><span>ویزیت</span><span>{formatRial(afterSupp)}</span></div>
{servicesTotal > 0 && <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, color: 'var(--text-3)' }}><span>سهم بیمار خدمات</span><span>{formatRial(servicesPatient)}</span></div>}
{consumablesTotal > 0 && <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, color: 'var(--text-3)' }}><span>کالاهای مصرفی</span><span>{formatRial(consumablesTotal)}</span></div>}
+35 -35
View File
@@ -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 با دیتای واقعی */}
<div dir="rtl" className="dark:border-[#404040] dark:bg-[#222433]" style={{ padding: 16, border: '1px dashed #A7A7E0' }}>
<div dir="rtl" className="dark:bg-[var(--surface)]" style={{ padding: 16, border: '1px dashed var(--primary-soft2)' }}>
{visitAt && (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, marginBottom: 16 }}>
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 13, color: '#616161' }}>تاریخ و ساعت مراجعه:</span>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 600, color: '#2f2f2f' }}>{formatDateTime(visitAt)}</span>
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>تاریخ و ساعت مراجعه:</span>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>{formatDateTime(visitAt)}</span>
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 32, marginBottom: 16 }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 500, color: '#2f2f2f' }}>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
{serviceNames.length ? serviceNames.join(' - ') : 'ویزیت'}
</span>
<span className="dark:bg-[#404040]" style={{ width: 1, height: 38, background: '#E0E0E0' }} />
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 500, color: '#525252' }}>{session.doctor_name || '—'}</span>
<span style={{ width: 1, height: 38, background: 'var(--border-2)' }} />
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>{session.doctor_name || '—'}</span>
</div>
{costItems.length > 0 && (
<div className="dark:border-[#404040]" style={{ marginBottom: 16, borderTop: '1px solid #EEE', borderBottom: '1px solid #EEE', padding: '4px 0' }}>
<div style={{ marginBottom: 16, borderTop: '1px solid var(--border)', borderBottom: '1px solid var(--border)', padding: '4px 0' }}>
{costItems.map((it, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '6px 0' }}>
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 13, color: '#616161' }}>{it.label}</span>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 500, color: '#525252' }}>{formatRial(it.rials)}</span>
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>{it.label}</span>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>{formatRial(it.rials)}</span>
</div>
))}
{hasInsurance && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0 4px', borderTop: '1px dashed #E0E0E0' }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, fontWeight: 700, color: '#2f2f2f' }}>جمع کل خدمات</span>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 700, color: '#2f2f2f' }}>{formatRial(grossTotal)}</span>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0 4px', borderTop: '1px dashed var(--border-2)' }}>
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>جمع کل خدمات</span>
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>{formatRial(grossTotal)}</span>
</div>
{baseInsurance > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '6px 0' }}>
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 13, color: '#616161' }}>سهم بیمه پایه</span>
<span style={{ fontSize: 14, fontWeight: 500, color: '#2E7D32' }}>{formatRial(baseInsurance)}</span>
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>سهم بیمه پایه</span>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--success)' }}>{formatRial(baseInsurance)}</span>
</div>
)}
{suppInsurance > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '6px 0' }}>
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 13, color: '#616161' }}>سهم بیمه تکمیلی</span>
<span style={{ fontSize: 14, fontWeight: 500, color: '#2E7D32' }}>{formatRial(suppInsurance)}</span>
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>سهم بیمه تکمیلی</span>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--success)' }}>{formatRial(suppInsurance)}</span>
</div>
)}
</>
)}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0 4px', borderTop: '1px dashed #E0E0E0' }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, fontWeight: 700, color: '#2f2f2f' }}>{hasInsurance ? 'سهم بیمار' : 'جمع کل'}</span>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 700, color: '#2f2f2f' }}>{formatRial(finalPrice)}</span>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0 4px', borderTop: '1px dashed var(--border-2)' }}>
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{hasInsurance ? 'سهم بیمار' : 'جمع کل'}</span>
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>{formatRial(finalPrice)}</span>
</div>
</div>
)}
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 72, marginBottom: 16 }}>
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 500, color: '#525252' }}>هزینه سرویس:</span>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 500, color: '#525252' }}>{formatRial(finalPrice)}</span>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>هزینه سرویس:</span>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>{formatRial(finalPrice)}</span>
</span>
<span className="dark:bg-[#404040]" style={{ width: 1, height: 38, background: '#E0E0E0' }} />
<span style={{ width: 1, height: 38, background: 'var(--border-2)' }} />
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 500, color: '#525252' }}>تخفیف:</span>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 500, color: '#525252' }}>{formatRial(discountRials)}</span>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>تخفیف:</span>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>{formatRial(discountRials)}</span>
</span>
</div>
<span className="dark:text-[#6A6AD9]" style={{ fontSize: 13, fontWeight: 600, color: '#5559CE', display: 'block', marginBottom: 12 }}>پرداخت شده ها:</span>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--primary)', display: 'block', marginBottom: 12 }}>پرداخت شده ها:</span>
{payments.length === 0 ? (
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 13, color: '#6B7280' }}>پرداختی ثبت نشده است</span>
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>پرداختی ثبت نشده است</span>
) : payments.map((p) => (
<div key={p.uuid} className="dark:border-[#404040]" style={{ padding: '8px 0', borderBottom: '1px solid #EEE' }}>
<div key={p.uuid} style={{ padding: '8px 0', borderBottom: '1px solid var(--border)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 56 }}>
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span className="dark:bg-[#6A6AD9]" style={{ width: 6, height: 6, borderRadius: '50%', background: '#5559CE', flexShrink: 0 }} />
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 600, color: '#525252' }}>{METHOD_LABELS[p.method] ?? p.method}</span>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--primary)', flexShrink: 0 }} />
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>{METHOD_LABELS[p.method] ?? p.method}</span>
</span>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 600, color: '#111827' }}>مبلغ: {formatRial(p.amount_rials)}</span>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>مبلغ: {formatRial(p.amount_rials)}</span>
</div>
<div className="dark:text-[#A1A1A1]" style={{ display: 'flex', gap: 12, marginTop: 4, fontSize: 12, color: '#9CA3AF', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: 12, marginTop: 4, fontSize: 12, color: 'var(--text-3)', flexWrap: 'wrap' }}>
{(p.paid_at ?? p.created_at) && <span>{formatDateTime(p.paid_at ?? p.created_at!)}</span>}
{p.created_by_name && <span>ثبت: {p.created_by_name}</span>}
</div>
@@ -153,8 +153,8 @@ export default function DetailsStep({ session, recordUuid, onBack, onFinish }: P
<div style={{ display: 'flex', width: '100%', justifyContent: 'flex-end', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 16 }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 600, color: '#525252' }}>مبلغ باقی مانده:</span>
<span className="dark:text-[#FF5252]" style={{ fontSize: 14, fontWeight: 600, color: '#d32f2f' }}>{formatRial(debt)}</span>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>مبلغ باقی مانده:</span>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--danger)' }}>{formatRial(debt)}</span>
</div>
</div>
</div>
+51 -51
View File
@@ -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
{/* هزینه سرویس */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12, margin: '12px 0 16px' }}>
<Step2PaymentCard />
<span style={{ fontSize: 14, color: '#F97316', fontWeight: 700 }}>هزینه سرویس:</span>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 700, color: '#525252' }}>{formatRial(finalPrice)}</span>
<span style={{ fontSize: 14, color: 'var(--accent)', fontWeight: 700 }}>هزینه سرویس:</span>
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--text)' }}>{formatRial(finalPrice)}</span>
</div>
{/* تخفیف‌های پیشنهادی (قوانین تخفیف) */}
{suggestions.length > 0 && (
<div className="dark:border-[#35343D]" style={{ border: '1px solid #E8EBFF', background: '#F7F8FF', borderRadius: 8, padding: 12, marginBottom: 16 }}>
<span style={{ ...fieldLabel, marginBottom: 10, fontWeight: 600, color: '#3B3F9F' }}>تخفیفهای قابل اعمال:</span>
<div style={{ border: '1px solid var(--primary-soft2)', background: 'var(--primary-soft)', borderRadius: 8, padding: 12, marginBottom: 16 }}>
<span style={{ ...fieldLabel, marginBottom: 10, fontWeight: 600, color: 'var(--primary-700)' }}>تخفیفهای قابل اعمال:</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{suggestions.map((s) => (
<div key={s.rule_uuid} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 13, color: '#374151' }}>
{s.rule_name} <span style={{ color: '#D32F2F' }}>{formatRial(s.discount_rials)}</span>
{' '}<span style={{ color: '#6B7280', fontSize: 12 }}>({s.discount_type === 'percent' ? `${s.value}٪` : 'مبلغ ثابت'})</span>
<span style={{ fontSize: 13, color: 'var(--text)' }}>
{s.rule_name} <span style={{ color: 'var(--danger)' }}>{formatRial(s.discount_rials)}</span>
{' '}<span style={{ color: 'var(--text-2)', fontSize: 12 }}>({s.discount_type === 'percent' ? `${s.value}٪` : 'مبلغ ثابت'})</span>
</span>
<button
type="button"
onClick={() => 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' }}
>
اعمال
</button>
@@ -178,8 +178,8 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
{/* تخفیف — port of tauri DiscountInput (نوع + مقدار + ثبت) */}
<span style={fieldLabel}>تخفیف:</span>
<div style={{ display: 'flex', alignItems: 'stretch', gap: 8 }}>
<div className="bg-white dark:bg-[#222433] dark:border-[#35343D]" style={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', border: '1px solid #e0e0e0', borderRadius: 8, marginBottom: 16 }}>
<div style={{ minWidth: 150, borderLeft: '1px solid #e0e0e0', padding: '0 4px' }}>
<div className="bg-[var(--surface)]" style={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', border: '1px solid var(--border-2)', borderRadius: 8, marginBottom: 16 }}>
<div style={{ minWidth: 150, borderLeft: '1px solid var(--border-2)', padding: '0 4px' }}>
<SearchableSelect
options={[{ value: 'percent', label: 'درصدی' }, { value: 'fixed', label: 'مبلغ ثابت' }]}
value={discountType}
@@ -198,7 +198,7 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
type="button"
onClick={applyDiscount}
disabled={discountMut.isPending || !discountType || discountValue <= 0}
style={{ height: 40, minWidth: 72, border: 'none', borderRadius: '0 4px 4px 0', background: '#E8EBFF', color: '#3B3F9F', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}
style={{ height: 40, minWidth: 72, border: 'none', borderRadius: '0 4px 4px 0', background: 'var(--primary-soft2)', color: 'var(--primary-700)', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}
>
ثبت
</button>
@@ -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' }}
>
<TrashRed color="#EF4444" size={18} />
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 13, color: '#6B7280' }}>حذف تخفیف</span>
<TrashRed color="var(--danger)" size={18} />
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>حذف تخفیف</span>
</div>
</div>
{/* خلاصه‌ی مبلغ: قبل از تخفیف / تخفیف / نهایی / منبع */}
<div className="dark:border-[#35343D]" style={{ border: '1px solid #e0e0e0', borderRadius: 8, padding: 12, marginBottom: 16, display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ border: '1px solid var(--border-2)', borderRadius: 8, padding: 12, marginBottom: 16, display: 'flex', flexDirection: 'column', gap: 6 }}>
{hasInsurance && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>هزینه کل خدمات:</span>
<span className="dark:text-[#D7D8ED]" style={{ color: '#2f2f2f' }}>{formatRial(grossTotal)}</span>
<span style={{ color: 'var(--text-2)' }}>هزینه کل خدمات:</span>
<span style={{ color: 'var(--text)' }}>{formatRial(grossTotal)}</span>
</div>
{baseInsurance > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>سهم بیمه پایه:</span>
<span style={{ color: '#2E7D32' }}>{formatRial(baseInsurance)}</span>
<span style={{ color: 'var(--text-2)' }}>سهم بیمه پایه:</span>
<span style={{ color: 'var(--success)' }}>{formatRial(baseInsurance)}</span>
</div>
)}
{suppInsurance > 0 && (
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>سهم بیمه تکمیلی:</span>
<span style={{ color: '#2E7D32' }}>{formatRial(suppInsurance)}</span>
<span style={{ color: 'var(--text-2)' }}>سهم بیمه تکمیلی:</span>
<span style={{ color: 'var(--success)' }}>{formatRial(suppInsurance)}</span>
</div>
)}
</>
)}
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>{hasInsurance ? 'سهم بیمار:' : 'مبلغ قبل از تخفیف:'}</span>
<span className="dark:text-[#D7D8ED]" style={{ color: '#2f2f2f' }}>{formatRial(finalPrice)}</span>
<span style={{ color: 'var(--text-2)' }}>{hasInsurance ? 'سهم بیمار:' : 'مبلغ قبل از تخفیف:'}</span>
<span style={{ color: 'var(--text)' }}>{formatRial(finalPrice)}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
<span className="dark:text-[#A1A1A1]" style={{ color: '#6B7280' }}>مبلغ تخفیف{appliedRuleLabel ? ` (${appliedRuleLabel})` : ''}:</span>
<span style={{ color: '#D32F2F' }}>{formatRial(discountRials)}</span>
<span style={{ color: 'var(--text-2)' }}>مبلغ تخفیف{appliedRuleLabel ? ` (${appliedRuleLabel})` : ''}:</span>
<span style={{ color: 'var(--danger)' }}>{formatRial(discountRials)}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, fontWeight: 700, borderTop: '1px dashed #e0e0e0', paddingTop: 6 }}>
<span className="dark:text-[#D7D8ED]" style={{ color: '#2f2f2f' }}>مبلغ نهایی قابل پرداخت:</span>
<span className="dark:text-[#D7D8ED]" style={{ color: '#2f2f2f' }}>{formatRial(payable)}</span>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, fontWeight: 700, borderTop: '1px dashed var(--border-2)', paddingTop: 6 }}>
<span style={{ color: 'var(--text)' }}>مبلغ نهایی قابل پرداخت:</span>
<span style={{ color: 'var(--text)' }}>{formatRial(payable)}</span>
</div>
</div>
{/* تاریخ پرداخت */}
<span className="dark:text-[#A1A1A1]" style={{ ...fieldLabel, color: '#3b3b3b' }}>تاریخ پرداخت</span>
<span style={{ ...fieldLabel, color: 'var(--text)' }}>تاریخ پرداخت</span>
<PersianDateInput value={paymentDate} onChange={setPaymentDate} />
{/* روش‌های پرداخت */}
<span className="dark:text-[#A1A1A1]" style={{ ...fieldLabel, color: '#3b3b3b', marginTop: 24 }}>انتخاب روش های پرداخت:</span>
<span style={{ ...fieldLabel, color: 'var(--text)', marginTop: 24 }}>انتخاب روش های پرداخت:</span>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ display: 'flex', gap: 4 }}>
<FilesServiceBalanceWallet />
<span style={{ fontSize: 14, color: '#F97316', marginBottom: 8, fontWeight: 600 }}>موجودی کیف پول:</span>
<span style={{ fontSize: 14, color: 'var(--accent)', marginBottom: 8, fontWeight: 600 }}>موجودی کیف پول:</span>
</div>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 600, color: '#525252', marginBottom: 4 }}>{formatRial(walletBalance)}</span>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)', marginBottom: 4 }}>{formatRial(walletBalance)}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
<span style={{ fontSize: 14, fontWeight: 600, color: '#111827' }}>مبلغ باقیمانده:</span>
<span className="dark:text-[#FF5252]" style={{ fontSize: 14, fontWeight: 700, color: '#d32f2f' }}>{formatRial(debt)}</span>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>مبلغ باقیمانده:</span>
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--danger)' }}>{formatRial(debt)}</span>
</div>
</div>
@@ -273,15 +273,15 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
{METHODS.map((m) => {
const open = expanded === m.key;
return (
<div key={m.key} className="dark:border-[#35343D]" style={{ borderBottom: '1px solid #e0e0e0' }}>
<div key={m.key} style={{ borderBottom: '1px solid var(--border-2)' }}>
<button
type="button"
aria-expanded={open}
onClick={() => { 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' }}
>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 600, color: '#111827' }}>{m.label}</span>
<ChevronDownIcon style={{ width: 16, color: '#6B7280', transform: open ? 'rotate(180deg)' : undefined, transition: 'transform .15s' }} />
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>{m.label}</span>
<ChevronDownIcon style={{ width: 16, color: 'var(--text-2)', transform: open ? 'rotate(180deg)' : undefined, transition: 'transform .15s' }} />
</button>
{open && (
<div style={{ display: 'flex', gap: 8, padding: '0 4px 14px' }}>
@@ -309,34 +309,34 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
</div>
{/* پرداخت شده‌ها — باکس خط‌چین tauri */}
<div className="dark:border-[#404040] dark:bg-[#222433]" style={{ border: '1px dashed #C7C9F4', borderRadius: 8, padding: 16, marginTop: 16, background: '#fff' }}>
<span className="dark:text-[#6A6AD9]" style={{ fontSize: 16, fontWeight: 500, color: '#636bd4', display: 'block', marginBottom: 16 }}>پرداخت شده ها:</span>
<div style={{ border: '1px dashed var(--primary-soft2)', borderRadius: 8, padding: 16, marginTop: 16, background: 'var(--surface)' }}>
<span style={{ fontSize: 16, fontWeight: 500, color: 'var(--primary)', display: 'block', marginBottom: 16 }}>پرداخت شده ها:</span>
{payments.length === 0 ? (
<span className="dark:text-[#A1A1A1]" style={{ fontSize: 13, color: '#6B7280' }}>پرداختی ثبت نشده است</span>
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>پرداختی ثبت نشده است</span>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{payments.map((p) => (
<div key={p.uuid} className="dark:border-[#404040]" style={{ padding: '6px 0', borderBottom: '1px solid #e0e0e0' }}>
<div key={p.uuid} style={{ padding: '6px 0', borderBottom: '1px solid var(--border-2)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span className="dark:bg-[#6A6AD9]" style={{ width: 8, height: 8, borderRadius: '50%', background: '#636bd4' }} />
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, color: '#111827' }}>{METHOD_LABELS[p.method] ?? p.method}</span>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: 'var(--primary)' }} />
<span style={{ fontSize: 14, color: 'var(--text)' }}>{METHOD_LABELS[p.method] ?? p.method}</span>
</span>
<span className="dark:text-[#D7D8ED]" style={{ flex: 1, textAlign: 'left', fontSize: 14, color: '#111827' }}>مبلغ : {formatRial(p.amount_rials)}</span>
<span style={{ flex: 1, textAlign: 'left', fontSize: 14, color: 'var(--text)' }}>مبلغ : {formatRial(p.amount_rials)}</span>
{p.method !== 'wallet' && (
<span style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
<button type="button" aria-label="ویرایش پرداخت" onClick={() => 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' }}>
<PencilIcon style={{ width: 16 }} />
</button>
<button type="button" aria-label="حذف پرداخت" onClick={() => 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' }}>
<TrashIcon style={{ width: 16 }} />
</button>
</span>
)}
</div>
<div className="dark:text-[#A1A1A1]" style={{ display: 'flex', gap: 12, marginTop: 4, fontSize: 12, color: '#9CA3AF', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: 12, marginTop: 4, fontSize: 12, color: 'var(--text-3)', flexWrap: 'wrap' }}>
{(p.paid_at ?? p.created_at) && <span>{formatDateTime(p.paid_at ?? p.created_at!)}</span>}
{p.created_by_name && <span>ثبت: {p.created_by_name}</span>}
</div>
@@ -345,8 +345,8 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 500, color: '#111827' }}>مبلغ باقیمانده :</span>
<span className="dark:text-[#FF5252]" style={{ fontSize: 14, fontWeight: 600, color: '#d32f2f' }}>{formatRial(debt)}</span>
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>مبلغ باقیمانده :</span>
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--danger)' }}>{formatRial(debt)}</span>
</div>
</div>
@@ -16,9 +16,9 @@ const FIELD_LABELS: Record<string, string> = {
};
const OP_LABELS: Record<string, { label: string; color: string }> = {
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
<div key={i} className="card" style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span className="badge" style={{ fontSize: 11, color: '#fff', background: op.color, borderRadius: 4, padding: '2px 8px' }}>{op.label}</span>
<span className="badge" style={{ fontSize: 11, color: 'var(--on-primary)', background: op.color, borderRadius: 4, padding: '2px 8px' }}>{op.label}</span>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)' }}>{FIELD_LABELS[r.field] ?? r.field}</span>
</span>
<span style={{ fontSize: 12, color: 'var(--text-3)' }}>{formatDateTime(r.created_at)}</span>
@@ -8,15 +8,15 @@ import ConfirmAppointmentModal from '../appointments/ConfirmAppointmentModal';
// Labels follow the Figma نوبت‌ها design (ثبت شده / قطعی شده / ویزیت شده …).
export const STATUS_META: Record<string, { label: string; color: string }> = {
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 (
<button
key={s}
@@ -191,7 +191,7 @@ export default function PersianCalendar({ value, onChange, onClose, enableYearPi
border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
fontWeight: sel || tod ? 700 : 400,
background: sel ? 'var(--primary)' : tod ? 'var(--surface-2)' : 'transparent',
color: sel ? '#fff' : tod ? 'var(--text)' : 'var(--text)',
color: sel ? 'var(--on-primary)' : tod ? 'var(--text)' : 'var(--text)',
transition: 'background 0.1s',
}}
onMouseEnter={e => { 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'; }}
>
+1 -1
View File
@@ -33,7 +33,7 @@ export default function StatCard({ tone, label, value, icon }: Props) {
<div
style={{
width: 44, height: 44, borderRadius: 12, flexShrink: 0,
background: c.fg, color: '#fff',
background: c.fg, color: 'var(--on-primary)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
@@ -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', () => {
+4 -4
View File
@@ -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 && <span style={{ width: 8, height: 8, background: '#fff', borderRadius: 2 }} />}
{active && <span style={{ width: 8, height: 8, background: 'var(--surface)', borderRadius: 2 }} />}
</span>
{o.name}
</button>
@@ -378,7 +378,7 @@ export default function AppointmentCreatePage() {
<button type="button" aria-label={`حذف ${s.name}`} onClick={() => 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',
}}>×</button>
</span>
@@ -451,13 +451,13 @@ export default function AppointmentCreatePage() {
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 10, fontSize: 14, cursor: 'pointer' }}>
<span style={{
position: 'relative', width: 42, height: 22, borderRadius: 999, flexShrink: 0,
background: depositRequired ? 'var(--primary)' : '#c4c4c4', transition: 'background .2s',
background: depositRequired ? 'var(--primary)' : 'var(--border-2)', transition: 'background .2s',
}}>
<input type="checkbox" checked={depositRequired} onChange={e => setDepositRequired(e.target.checked)}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', margin: 0, opacity: 0, cursor: 'pointer' }} />
<span style={{
position: 'absolute', top: 2, insetInlineStart: depositRequired ? 22 : 2, width: 18, height: 18,
borderRadius: 999, background: '#fff', transition: 'inset-inline-start .2s', boxShadow: '0 1px 2px rgba(0,0,0,.2)',
borderRadius: 999, background: 'var(--surface)', transition: 'inset-inline-start .2s', boxShadow: '0 1px 2px rgba(0,0,0,.2)',
}} />
</span>
بیعانه مورد نیاز است.
+17 -17
View File
@@ -123,7 +123,7 @@ export default function AppointmentDetailPage() {
]}
action={
<button onClick={() => 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">
<ArrowRightIcon className="w-4 h-4" />
بازگشت
</button>
@@ -138,8 +138,8 @@ export default function AppointmentDetailPage() {
</div>
) : appt ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
<h3 className="font-semibold text-gray-800 mb-4">اطلاعات بیمار</h3>
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6">
<h3 className="font-semibold text-[var(--text)] mb-4">اطلاعات بیمار</h3>
<InfoRow label="نام بیمار" value={appt.patient_name} />
<InfoRow label="موبایل" value={<span dir="ltr">{appt.patient_mobile}</span>} />
<InfoRow label="پزشک" value={appt.doctor?.name ?? null} />
@@ -151,10 +151,10 @@ export default function AppointmentDetailPage() {
<InfoRow label="تاریخ ثبت" value={formatDateTime(appt.created_at)} />
</div>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
<h3 className="font-semibold text-gray-800 mb-4">وضعیت و اقدامات</h3>
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6">
<h3 className="font-semibold text-[var(--text)] mb-4">وضعیت و اقدامات</h3>
<div className="mb-4">
<p className="text-sm text-gray-500 mb-2">وضعیت فعلی:</p>
<p className="text-sm text-[var(--text-2)] mb-2">وضعیت فعلی:</p>
<StatusBadge type="appointment" value={appt.status} />
</div>
@@ -189,37 +189,37 @@ export default function AppointmentDetailPage() {
</div>
</div>
<div className="mt-4 pt-4 border-t border-gray-100">
<div className="mt-4 pt-4 border-t border-[var(--border)]">
<button
onClick={() => 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"
>
لغو نوبت
</button>
</div>
</div>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6 lg:col-span-2">
<h3 className="font-semibold text-gray-800 mb-4">تاریخچه رویدادها</h3>
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6 lg:col-span-2">
<h3 className="font-semibold text-[var(--text)] mb-4">تاریخچه رویدادها</h3>
{eventsQuery.isLoading ? (
<div className="h-6 w-40 rounded skeleton" />
) : events.length === 0 ? (
<p className="text-sm text-gray-400">رویدادی برای این نوبت ثبت نشده است.</p>
<p className="text-sm text-[var(--text-3)]">رویدادی برای این نوبت ثبت نشده است.</p>
) : (
<ol className="space-y-4">
{events.map((ev, i) => (
<li key={i} className="flex gap-3">
<span className="mt-1.5 w-2.5 h-2.5 rounded-full bg-red-500 shrink-0" />
<span className="mt-1.5 w-2.5 h-2.5 rounded-full bg-[var(--danger)] shrink-0" />
<div className="flex-1">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-gray-800">{ev.title}</span>
<span className="text-xs text-gray-400">{formatDateTime(ev.created_at)}</span>
<span className="text-sm font-medium text-[var(--text)]">{ev.title}</span>
<span className="text-xs text-[var(--text-3)]">{formatDateTime(ev.created_at)}</span>
</div>
{ev.actor_name && (
<div className="text-xs text-gray-500 mt-0.5">توسط: {ev.actor_name}</div>
<div className="text-xs text-[var(--text-2)] mt-0.5">توسط: {ev.actor_name}</div>
)}
{ev.reason && (
<div className="text-xs text-gray-600 mt-0.5">دلیل: {ev.reason}</div>
<div className="text-xs text-[var(--text-2)] mt-0.5">دلیل: {ev.reason}</div>
)}
</div>
</li>
@@ -229,7 +229,7 @@ export default function AppointmentDetailPage() {
</div>
</div>
) : (
<div className="bg-white rounded-2xl border border-gray-100 p-16 text-center text-gray-400">
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] p-16 text-center text-[var(--text-3)]">
نوبتی یافت نشد
</div>
)}
+1 -1
View File
@@ -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,
}}>
<span style={{ fontSize: 13, fontWeight: 700, lineHeight: 1.2, color: isFriday ? '#ef4444' : 'var(--text)' }}>
<span style={{ fontSize: 13, fontWeight: 700, lineHeight: 1.2, color: isFriday ? 'var(--danger)' : 'var(--text)' }}>
{weekDay}
</span>
<span style={{ fontSize: 11, color: 'var(--text-2)', lineHeight: 1.2 }}>
+2 -2
View File
@@ -142,7 +142,7 @@ export default function BlogFormPage() {
<div>
<label>عنوان مقاله *</label>
<input {...register('title')} placeholder="عنوان جذاب بنویسید..." className="cp-input h-11" />
{errors.title && <p className="text-red-500 text-xs mt-1">{errors.title.message}</p>}
{errors.title && <p className="text-[var(--danger)] text-xs mt-1">{errors.title.message}</p>}
</div>
<div>
@@ -170,7 +170,7 @@ export default function BlogFormPage() {
</div>
)}
/>
{errors.body && <p className="text-red-500 text-xs mt-1">{errors.body.message}</p>}
{errors.body && <p className="text-[var(--danger)] text-xs mt-1">{errors.body.message}</p>}
</div>
</div>
+1 -1
View File
@@ -90,7 +90,7 @@ function LogoUploadField({ value, onChange, uploadUrl }: {
<img src={value} alt="logo" style={{ width: 64, height: 64, objectFit: 'contain', borderRadius: 12, border: '1px solid var(--border)', background: 'var(--surface)', padding: 4 }} />
<button type="button" onClick={() => 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',
}}>
<XMarkIcon style={{ width: 12, height: 12 }} />
+1 -1
View File
@@ -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',
}}>
<CameraIcon style={{ width: 12, height: 12, color: '#fff' }} />
<CameraIcon style={{ width: 12, height: 12, color: 'var(--on-primary)' }} />
</button>
<input ref={logoInputRef} type="file" accept="image/*" style={{ display: 'none' }}
onChange={e => e.target.files?.[0] && handleLogoUpload(e.target.files[0])} />
+1 -1
View File
@@ -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)}
</div>
+5 -5
View File
@@ -66,7 +66,7 @@ const PAY_LABEL: Record<string, string> = {
pending: 'در انتظار', received: 'موفق', canceled: 'لغو شده', refund: 'استرداد',
};
const PAY_COLOR: Record<string, string> = {
pending: '#f59e0b', received: '#22c55e', canceled: '#ef4444', refund: '#3b82f6',
pending: 'var(--warning)', received: 'var(--success)', canceled: 'var(--danger)', refund: 'var(--info)',
};
const PAY_CLS: Record<string, string> = {
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]);
+119 -119
View File
@@ -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
? <img src={img} alt={name} className="w-24 h-24 rounded-2xl object-cover shadow-xl ring-4 ring-white dark:ring-gray-900" />
: <div className={`w-24 h-24 rounded-2xl bg-gradient-to-br ${AVATAR_COLORS[idx % AVATAR_COLORS.length]} flex items-center justify-center text-white text-3xl font-bold shadow-xl ring-4 ring-white dark:ring-gray-900`}>{initials}</div>
: <div className={`w-24 h-24 rounded-2xl bg-gradient-to-br ${AVATAR_COLORS[idx % AVATAR_COLORS.length]} flex items-center justify-center text-[var(--on-primary)] text-3xl font-bold shadow-xl ring-4 ring-white dark:ring-gray-900`}>{initials}</div>
}
{onUpload && (
<div className="absolute inset-0 rounded-2xl bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
{uploading
? <span className="w-6 h-6 border-2 border-white/40 border-t-white rounded-full animate-spin" />
: <CameraIcon className="w-7 h-7 text-white" />
? <span className="w-6 h-6 border-2 border-[var(--surface)]/40 border-t-white rounded-full animate-spin" />
: <CameraIcon className="w-7 h-7 text-[var(--on-primary)]" />
}
</div>
)}
@@ -161,19 +161,19 @@ function InfoCard({ icon: Icon, label, value, mono = false, copyable = false }:
mono?: boolean; copyable?: boolean;
}) {
return (
<div className="flex items-start gap-3 p-4 rounded-xl bg-slate-50 dark:bg-gray-800/60 hover:bg-slate-100 dark:hover:bg-gray-800 transition-colors group">
<div className="w-9 h-9 rounded-lg bg-white dark:bg-gray-700 shadow-sm flex items-center justify-center shrink-0">
<Icon className="w-4 h-4 text-slate-500 dark:text-slate-400" />
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] transition-colors group">
<div className="w-9 h-9 rounded-lg bg-[var(--surface)] shadow-sm flex items-center justify-center shrink-0">
<Icon className="w-4 h-4 text-[var(--text-2)]" />
</div>
<div className="flex-1 min-w-0">
<p className="text-[11px] font-medium text-slate-400 dark:text-slate-500 uppercase tracking-wide mb-0.5">{label}</p>
<p className={`text-sm font-medium text-slate-800 dark:text-slate-200 ${mono ? 'font-mono' : ''}`} dir={mono ? 'ltr' : undefined}>
{value ?? <span className="text-slate-400 font-normal">ثبت نشده</span>}
<p className="text-[11px] font-medium text-[var(--text-3)] uppercase tracking-wide mb-0.5">{label}</p>
<p className={`text-sm font-medium text-[var(--text)] dark:text-[var(--text)] ${mono ? 'font-mono' : ''}`} dir={mono ? 'ltr' : undefined}>
{value ?? <span className="text-[var(--text-3)] font-normal">ثبت نشده</span>}
</p>
</div>
{copyable && typeof value === 'string' && (
<button onClick={() => { 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">
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
</button>
)}
@@ -187,9 +187,9 @@ function StarRating({ rate }: { rate: number }) {
{Array.from({ length: 5 }).map((_, i) =>
i < Math.round(rate)
? <StarSolid key={i} className="w-4 h-4 text-amber-400" />
: <StarIcon key={i} className="w-4 h-4 text-slate-300 dark:text-slate-600" />
: <StarIcon key={i} className="w-4 h-4 text-[var(--text-3)]" />
)}
<span className="text-sm font-medium text-slate-600 dark:text-slate-400 mr-1">{rate.toFixed(1)}</span>
<span className="text-sm font-medium text-[var(--text-2)] mr-1">{rate.toFixed(1)}</span>
</div>
);
}
@@ -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 (
<div className="rounded-xl overflow-hidden border border-slate-200 dark:border-gray-700" style={{ height: 300 }}>
<div className="rounded-xl overflow-hidden border border-[var(--border)]" style={{ height: 300 }}>
<MapContainer center={center} zoom={pos ? 13 : 5} style={{ height: '100%', width: '100%' }}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
@@ -306,7 +306,7 @@ function SearchableSelect({ options, value, onChange, placeholder, disabled = fa
type="button"
disabled={disabled}
onClick={openDropdown}
className={`cp-input h-11 flex items-center justify-between text-right w-full ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} ${!selected ? 'text-slate-400 dark:text-slate-500' : ''}`}
className={`cp-input h-11 flex items-center justify-between text-right w-full ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} ${!selected ? 'text-[var(--text-3)] dark:text-[var(--text-2)]' : ''}`}
>
<span className="truncate flex-1">{selected?.label ?? placeholder ?? 'انتخاب کنید'}</span>
<ChevronDownIcon className={`w-4 h-4 shrink-0 mr-2 transition-transform ${open ? 'rotate-180' : ''}`} />
@@ -314,26 +314,26 @@ function SearchableSelect({ options, value, onChange, placeholder, disabled = fa
{open && createPortal(
<div ref={dropRef} style={dropStyle}
className="bg-white dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-xl shadow-2xl overflow-hidden">
<div className="p-2 border-b border-slate-100 dark:border-gray-700">
className="bg-[var(--surface)] border border-[var(--border)] rounded-xl shadow-2xl overflow-hidden">
<div className="p-2 border-b border-[var(--border)]">
<input autoFocus type="text" value={q} onChange={e => setQ(e.target.value)}
placeholder="جستجو..." className="cp-input text-sm h-8" />
</div>
<div className="max-h-52 overflow-y-auto">
<button type="button"
onClick={() => { 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 ?? 'انتخاب کنید'}
</button>
{filtered.map(o => (
<button key={o.value} type="button"
onClick={() => { 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}
</button>
))}
{filtered.length === 0 && (
<p className="text-xs text-slate-400 text-center py-3">نتیجهای یافت نشد</p>
<p className="text-xs text-[var(--text-3)] text-center py-3">نتیجهای یافت نشد</p>
)}
</div>
</div>,
@@ -379,13 +379,13 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
);
return (
<div className="border border-slate-200 dark:border-gray-700 rounded-xl overflow-hidden">
<div className="p-2 border-b border-slate-100 dark:border-gray-700 bg-slate-50 dark:bg-gray-800/60">
<div className="border border-[var(--border)] rounded-xl overflow-hidden">
<div className="p-2 border-b border-[var(--border)] bg-[var(--surface-2)] dark:bg-[var(--surface)]/60">
<input type="text" value={q} onChange={e => setQ(e.target.value)} placeholder="جستجوی تخصص..."
className="cp-input text-sm h-8" />
</div>
{selectedLabels.length > 0 && (
<div className="px-3 py-2 border-b border-slate-100 dark:border-gray-700 flex flex-wrap gap-1">
<div className="px-3 py-2 border-b border-[var(--border)] flex flex-wrap gap-1">
{selectedLabels.map(({ id, name }) => (
<span key={id} className="inline-flex items-center gap-1 text-xs px-2.5 py-0.5 rounded-full bg-primary-100 dark:bg-primary-500/20 text-primary-700 dark:text-primary-300">
{name}
@@ -398,18 +398,18 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
{q ? (
filteredFlat.length > 0
? filteredFlat.map(s => (
<label key={s.id} className="flex items-center gap-2.5 px-3 py-2 hover:bg-slate-50 dark:hover:bg-gray-800 cursor-pointer">
<label key={s.id} className="flex items-center gap-2.5 px-3 py-2 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer">
<input type="checkbox" checked={selected.includes(s.id)} onChange={() => toggleSelect(s.id)}
className="rounded border-slate-300 dark:border-gray-600 text-primary-600 shrink-0" />
<span className="text-sm text-slate-700 dark:text-slate-300">{s.name}</span>
className="rounded border-[var(--border-2)] dark:border-gray-600 text-primary-600 shrink-0" />
<span className="text-sm text-[var(--text)]">{s.name}</span>
{s.parent_id !== null && (
<span className="text-[10px] text-slate-400 mr-auto truncate max-w-[100px]">
<span className="text-[10px] text-[var(--text-3)] mr-auto truncate max-w-[100px]">
{specialties.find(p => p.id === s.parent_id)?.name}
</span>
)}
</label>
))
: <p className="text-xs text-slate-400 text-center py-4">نتیجهای یافت نشد</p>
: <p className="text-xs text-[var(--text-3)] text-center py-4">نتیجهای یافت نشد</p>
) : (
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 (
<div key={parent.id}>
<div className="flex items-center gap-2 px-3 py-2.5 hover:bg-slate-50 dark:hover:bg-gray-800 cursor-pointer select-none"
<div className="flex items-center gap-2 px-3 py-2.5 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer select-none"
onClick={() => children.length > 0 ? toggleParent(parent.id) : toggleSelect(parent.id)}>
{children.length > 0
? (isOpen
? <ChevronDownIcon className="w-4 h-4 text-slate-400 shrink-0" />
: <ChevronRightIcon className="w-4 h-4 text-slate-400 shrink-0" />)
? <ChevronDownIcon className="w-4 h-4 text-[var(--text-3)] shrink-0" />
: <ChevronRightIcon className="w-4 h-4 text-[var(--text-3)] shrink-0" />)
: <input type="checkbox" checked={selected.includes(parent.id)} readOnly
className="rounded border-slate-300 dark:border-gray-600 text-primary-600 pointer-events-none shrink-0" />
className="rounded border-[var(--border-2)] dark:border-gray-600 text-primary-600 pointer-events-none shrink-0" />
}
<span className={`text-sm font-medium ${numSel > 0 ? 'text-primary-600 dark:text-primary-400' : 'text-slate-700 dark:text-slate-300'}`}>
<span className={`text-sm font-medium ${numSel > 0 ? 'text-primary-600 dark:text-primary-400' : 'text-[var(--text)] dark:text-[var(--text-3)]'}`}>
{parent.name}
</span>
{numSel > 0 && (
@@ -436,10 +436,10 @@ function HierarchicalSpecialtyPicker({ selected, onChange, specialties }: {
)}
</div>
{isOpen && children.map(child => (
<label key={child.id} className="flex items-center gap-2.5 px-3 py-2 pr-9 hover:bg-slate-50 dark:hover:bg-gray-800 cursor-pointer">
<label key={child.id} className="flex items-center gap-2.5 px-3 py-2 pr-9 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer">
<input type="checkbox" checked={selected.includes(child.id)} onChange={() => toggleSelect(child.id)}
className="rounded border-slate-300 dark:border-gray-600 text-primary-600 shrink-0" />
<span className="text-sm text-slate-600 dark:text-slate-400">{child.name}</span>
className="rounded border-[var(--border-2)] dark:border-gray-600 text-primary-600 shrink-0" />
<span className="text-sm text-[var(--text-2)]">{child.name}</span>
</label>
))}
</div>
@@ -487,7 +487,7 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
if (selectedSpecialtyIds.length === 0) {
return (
<div className="rounded-xl border border-dashed border-slate-300 dark:border-gray-600 py-8 text-center text-slate-400 dark:text-slate-500 text-sm">
<div className="rounded-xl border border-dashed border-[var(--border-2)] dark:border-gray-600 py-8 text-center text-[var(--text-3)] text-sm">
ابتدا تخصصهایی را انتخاب کنید تا خدمات نمایش داده شوند
</div>
);
@@ -495,31 +495,31 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
if (relevantServices.length === 0) {
return (
<div className="rounded-xl border border-dashed border-slate-300 dark:border-gray-600 py-8 text-center text-slate-400 dark:text-slate-500 text-sm">
<div className="rounded-xl border border-dashed border-[var(--border-2)] dark:border-gray-600 py-8 text-center text-[var(--text-3)] text-sm">
خدمتی برای تخصصهای انتخابشده یافت نشد
</div>
);
}
return (
<div className="border border-slate-200 dark:border-gray-700 rounded-xl overflow-hidden">
<div className="border border-[var(--border)] rounded-xl overflow-hidden">
{tabs.length > 1 && (
<div className="flex gap-1 px-2 py-2 border-b border-slate-100 dark:border-gray-700 bg-slate-50 dark:bg-gray-800/60 overflow-x-auto">
<div className="flex gap-1 px-2 py-2 border-b border-[var(--border)] bg-[var(--surface-2)] dark:bg-[var(--surface)]/60 overflow-x-auto">
{[{ id: 'all' as const, name: 'همه' }, ...tabs].map(t => (
<button key={String(t.id)} type="button"
onClick={() => 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}
</button>
))}
</div>
)}
<div className="p-2 border-b border-slate-100 dark:border-gray-700">
<div className="p-2 border-b border-[var(--border)]">
<input type="text" value={q} onChange={e => setQ(e.target.value)} placeholder="جستجوی خدمت..."
className="cp-input text-sm h-8" />
</div>
{selectedLabels.length > 0 && (
<div className="px-3 py-2 border-b border-slate-100 dark:border-gray-700 flex flex-wrap gap-1">
<div className="px-3 py-2 border-b border-[var(--border)] flex flex-wrap gap-1">
{selectedLabels.map(({ id, name }) => (
<span key={id} className="inline-flex items-center gap-1 text-xs px-2.5 py-0.5 rounded-full bg-emerald-100 dark:bg-emerald-500/20 text-emerald-700 dark:text-emerald-300">
{name}
@@ -530,14 +530,14 @@ function ServicesPicker({ selected, onChange, services, specialties, selectedSpe
)}
<div className="max-h-44 overflow-y-auto">
{visibleServices.map(svc => (
<label key={svc.id} className="flex items-center gap-2.5 px-3 py-2 hover:bg-slate-50 dark:hover:bg-gray-800 cursor-pointer">
<label key={svc.id} className="flex items-center gap-2.5 px-3 py-2 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] cursor-pointer">
<input type="checkbox" checked={selected.includes(svc.id)} onChange={() => toggle(svc.id)}
className="rounded border-slate-300 dark:border-gray-600 text-emerald-600 shrink-0" />
<span className="text-sm text-slate-700 dark:text-slate-300">{svc.name}</span>
className="rounded border-[var(--border-2)] dark:border-gray-600 text-[var(--success)] shrink-0" />
<span className="text-sm text-[var(--text)]">{svc.name}</span>
</label>
))}
{visibleServices.length === 0 && q && (
<p className="text-xs text-slate-400 text-center py-4">نتیجهای یافت نشد</p>
<p className="text-xs text-[var(--text-3)] text-center py-4">نتیجهای یافت نشد</p>
)}
</div>
</div>
@@ -644,32 +644,32 @@ function AddressModal({ open, onClose, existing, doctorUuid, onSaved }: {
{/* Row 1: Name + Phone */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">نام مطب / کلینیک</label>
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">نام مطب / کلینیک</label>
<input type="text" className="input" placeholder="مثال: کلینیک مهر" {...register('name')} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
تلفن <span className="text-red-500">*</span>
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
تلفن <span className="text-[var(--danger)]">*</span>
</label>
<input className="cp-input text-left" placeholder="021..." {...latinDigitsField(register('telephone'))} />
{errors.telephone && <p className="text-xs text-red-500 mt-1">{errors.telephone.message}</p>}
{errors.telephone && <p className="text-xs text-[var(--danger)] mt-1">{errors.telephone.message}</p>}
</div>
</div>
{/* Row 2: Full address */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
آدرس کامل <span className="text-red-500">*</span>
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
آدرس کامل <span className="text-[var(--danger)]">*</span>
</label>
<textarea rows={2} className="cp-input resize-none" placeholder="خیابان، کوچه، پلاک..." {...register('address')} />
{errors.address && <p className="text-xs text-red-500 mt-1">{errors.address.message}</p>}
{errors.address && <p className="text-xs text-[var(--danger)] mt-1">{errors.address.message}</p>}
</div>
{/* Row 3: Province + City side by side */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
استان <span className="text-red-500">*</span>
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
استان <span className="text-[var(--danger)]">*</span>
</label>
<SearchableSelect
options={provinces.map(p => ({ value: p.id, label: p.name }))}
@@ -681,11 +681,11 @@ function AddressModal({ open, onClose, existing, doctorUuid, onSaved }: {
setMapFlyTarget(null);
}}
/>
{errors.province_id && <p className="text-xs text-red-500 mt-1">{errors.province_id.message}</p>}
{errors.province_id && <p className="text-xs text-[var(--danger)] mt-1">{errors.province_id.message}</p>}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
شهر <span className="text-red-500">*</span>
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
شهر <span className="text-[var(--danger)]">*</span>
</label>
<SearchableSelect
options={cities.map(c => ({ value: c.id, label: c.name }))}
@@ -701,23 +701,23 @@ function AddressModal({ open, onClose, existing, doctorUuid, onSaved }: {
}
}}
/>
{errors.city_id && <p className="text-xs text-red-500 mt-1">{errors.city_id.message}</p>}
{errors.city_id && <p className="text-xs text-[var(--danger)] mt-1">{errors.city_id.message}</p>}
</div>
</div>
{/* Row 4: Map */}
<div>
<div className="flex items-center justify-between mb-1.5">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">
<label className="block text-sm font-medium text-[var(--text)]">
موقعیت روی نقشه
</label>
{lat && lng && (
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono" dir="ltr">
<span className="text-xs text-[var(--text-2)] font-mono" dir="ltr">
{lat.toFixed(5)}, {lng.toFixed(5)}
</span>
)}
</div>
<p className="text-xs text-slate-400 dark:text-slate-500 mb-2">
<p className="text-xs text-[var(--text-3)] mb-2">
برای دقت بیشتر روی نقشه کلیک کنید
</p>
<MapPicker lat={lat} lng={lng} flyTarget={mapFlyTarget}
@@ -725,7 +725,7 @@ function AddressModal({ open, onClose, existing, doctorUuid, onSaved }: {
{lat && lng && (
<button type="button"
onClick={() => { 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">
<XMarkIcon className="w-3 h-3" />حذف موقعیت
</button>
)}
@@ -741,26 +741,26 @@ function AddressCard({ addr, onEdit, onDelete, readOnly = false }: {
addr: AddressData; onEdit: () => void; onDelete: () => void; readOnly?: boolean;
}) {
return (
<div className="flex items-start gap-3 p-4 rounded-xl bg-slate-50 dark:bg-gray-800/60 border border-slate-100 dark:border-gray-700 group">
<div className="w-9 h-9 rounded-lg bg-white dark:bg-gray-700 shadow-sm flex items-center justify-center shrink-0 mt-0.5">
<MapPinIcon className="w-4 h-4 text-slate-400" />
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60 border border-[var(--border)] group">
<div className="w-9 h-9 rounded-lg bg-[var(--surface)] shadow-sm flex items-center justify-center shrink-0 mt-0.5">
<MapPinIcon className="w-4 h-4 text-[var(--text-3)]" />
</div>
<div className="flex-1 min-w-0">
{addr.name && <p className="text-sm font-semibold text-slate-800 dark:text-slate-200 mb-0.5">{addr.name}</p>}
{addr.address && <p className="text-xs text-slate-600 dark:text-slate-400 leading-relaxed">{addr.address}</p>}
{addr.name && <p className="text-sm font-semibold text-[var(--text)] mb-0.5">{addr.name}</p>}
{addr.address && <p className="text-xs text-[var(--text-2)] leading-relaxed">{addr.address}</p>}
<div className="flex flex-wrap gap-x-3 gap-y-1 mt-1.5">
{addr.telephone && (
<span className="text-xs text-slate-500 dark:text-slate-400 flex items-center gap-1 font-mono" dir="ltr">
<span className="text-xs text-[var(--text-2)] flex items-center gap-1 font-mono" dir="ltr">
<PhoneIcon className="w-3 h-3" />{addr.telephone}
</span>
)}
{addr.province && (
<span className="text-xs text-slate-500 dark:text-slate-400">
<span className="text-xs text-[var(--text-2)]">
{addr.province.name}{addr.city ? `${addr.city.name}` : ''}
</span>
)}
{addr.map.latitude && addr.map.longitude && (
<span className="text-xs text-slate-400 dark:text-slate-500 flex items-center gap-1" dir="ltr">
<span className="text-xs text-[var(--text-3)] flex items-center gap-1" dir="ltr">
<MapPinIcon className="w-3 h-3" />{parseFloat(addr.map.latitude).toFixed(4)}, {parseFloat(addr.map.longitude).toFixed(4)}
</span>
)}
@@ -769,11 +769,11 @@ function AddressCard({ addr, onEdit, onDelete, readOnly = false }: {
{!readOnly && (
<div className="opacity-0 group-hover:opacity-100 flex items-center gap-1 transition-opacity shrink-0">
<button onClick={onEdit}
className="w-7 h-7 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-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">
<PencilIcon className="w-3.5 h-3.5" />
</button>
<button onClick={onDelete}
className="w-7 h-7 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-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">
<TrashIcon className="w-3.5 h-3.5" />
</button>
</div>
@@ -1362,10 +1362,10 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
if (isError || !doctor) {
return (
<div className="animate-slide-up cp-card p-16 flex flex-col items-center gap-4 text-center">
<div className="w-16 h-16 rounded-2xl bg-red-100 dark:bg-red-400/10 flex items-center justify-center">
<XCircleIcon className="w-8 h-8 text-red-500" />
<div className="w-16 h-16 rounded-2xl bg-[var(--danger-bg)] dark:bg-red-400/10 flex items-center justify-center">
<XCircleIcon className="w-8 h-8 text-[var(--danger)]" />
</div>
<p className="font-semibold text-slate-800 dark:text-slate-200">پزشک یافت نشد</p>
<p className="font-semibold text-[var(--text)]">پزشک یافت نشد</p>
<button onClick={() => navigate('/admin/doctors')} className="cp-btn-secondary mt-2">
بازگشت به لیست پزشکان
</button>
@@ -1383,24 +1383,24 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
<div className="animate-slide-up space-y-5">
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
<div className="flex items-center gap-2 text-sm text-[var(--text-2)]">
{isOwnProfile ? (
<>
<button onClick={() => 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">
<ArrowRightIcon className="w-4 h-4" />داشبورد
</button>
<span>/</span>
<span className="text-slate-700 dark:text-slate-300 font-medium">پروفایل من</span>
<span className="text-[var(--text)] font-medium">پروفایل من</span>
</>
) : (
<>
<button onClick={() => 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">
<ArrowRightIcon className="w-4 h-4" />{primaryRole === 'clinic' ? 'داشبورد' : 'پزشکان'}
</button>
<span>/</span>
<span className="text-slate-700 dark:text-slate-300 font-medium">پروفایل پزشک</span>
<span className="text-[var(--text)] font-medium">پروفایل پزشک</span>
</>
)}
</div>
@@ -1422,27 +1422,27 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
/>
)}
{primaryRole !== 'clinic' && !isReadOnly && (
<span className="text-[10px] text-slate-400 dark:text-slate-500">کلیک برای تغییر عکس</span>
<span className="text-[10px] text-[var(--text-3)]">کلیک برای تغییر عکس</span>
)}
</div>
<div className="flex-1 min-w-0 sm:mb-1">
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-50">{displayDoctorName(doctor.name)}</h1>
<h1 className="text-xl font-bold text-[var(--text)] dark:text-slate-50">{displayDoctorName(doctor.name)}</h1>
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
{/* Active status badge */}
{doctor.active
? <span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-emerald-100 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300">
<CheckCircleIcon className="w-3 h-3" />فعال
</span>
: <span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-slate-100 dark:bg-gray-700 text-slate-500 dark:text-slate-400">
: <span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--surface-2)] text-[var(--text-2)]">
<XCircleIcon className="w-3 h-3" />غیرفعال
</span>
}
{doctor.degree && (
<span className="inline-flex text-xs font-medium px-2.5 py-0.5 rounded-full bg-blue-100 dark:bg-blue-400/10 text-blue-700 dark:text-blue-300">{degreeLabel}</span>
<span className="inline-flex text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--info-bg)] dark:bg-blue-400/10 text-[var(--info)] dark:text-blue-300">{degreeLabel}</span>
)}
{doctor.gender && (
<span className="text-xs text-slate-500 dark:text-slate-400">{doctor.gender === 'man' ? '♂ مرد' : '♀ زن'}</span>
<span className="text-xs text-[var(--text-2)]">{doctor.gender === 'man' ? '♂ مرد' : '♀ زن'}</span>
)}
</div>
{doctor.specialties.length > 0 && (
@@ -1455,7 +1455,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
{doctor.expertise.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{doctor.expertise.map(e => (
<span key={e.id} className="text-[11px] px-2 py-0.5 rounded-full bg-slate-100 dark:bg-gray-700 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-gray-600">{e.name}</span>
<span key={e.id} className="text-[11px] px-2 py-0.5 rounded-full bg-[var(--surface-2)] text-[var(--text-2)] border border-[var(--border)] dark:border-gray-600">{e.name}</span>
))}
</div>
)}
@@ -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
<EllipsisVerticalIcon className="w-4 h-4" />
</button>
{menuOpen && (
<div className="absolute left-0 top-12 z-30 w-52 cp-card shadow-2xl border border-slate-200 dark:border-gray-700 py-1.5 animate-scale-in" data-dmenu>
<div className="absolute left-0 top-12 z-30 w-52 cp-card shadow-2xl border border-[var(--border)] py-1.5 animate-scale-in" data-dmenu>
<button onClick={() => { 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">
<TrashIcon className="w-4 h-4 shrink-0" />حذف این پزشک
</button>
</div>
@@ -1515,7 +1515,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
<div className="lg:col-span-2 space-y-5">
<div className="cp-card p-6 space-y-4">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">اطلاعات پایه</h2>
<h2 className="text-sm font-semibold text-[var(--text)]">اطلاعات پایه</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<InfoCard icon={UserIcon} label="نام کامل" value={doctor.name} />
<InfoCard icon={AcademicCapIcon} label="کد نظام پزشکی" value={doctor.medical_system_code} mono copyable />
@@ -1527,8 +1527,8 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
value={
<span className="flex items-center gap-2">
{doctor.representation
? <button type="button" style={{ color: '#2563eb', textDecoration: 'underline', background: 'none', border: 0, padding: 0, cursor: 'pointer', font: 'inherit' }} onClick={() => navigate(`/admin/representations/${doctor.representation!.uuid}`)}>{doctor.representation.full_name ?? 'نماینده'}</button>
: <span className="text-slate-400">بدون نماینده</span>}
? <button type="button" style={{ color: 'var(--info)', textDecoration: 'underline', background: 'none', border: 0, padding: 0, cursor: 'pointer', font: 'inherit' }} onClick={() => navigate(`/admin/representations/${doctor.representation!.uuid}`)}>{doctor.representation.full_name ?? 'نماینده'}</button>
: <span className="text-[var(--text-3)]">بدون نماینده</span>}
{isAdmin && (
<button type="button" className="btn ghost sm"
onClick={() => { setSelRepId(doctor.representation?.id ?? null); setRepModalOpen(true); }}>
@@ -1543,15 +1543,15 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
{doctor.detail && (
<div className="cp-card p-6">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">بیوگرافی</h2>
<p className="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">{doctor.detail}</p>
<h2 className="text-sm font-semibold text-[var(--text)] mb-3">بیوگرافی</h2>
<p className="text-sm text-[var(--text-2)] leading-relaxed">{doctor.detail}</p>
</div>
)}
{/* Addresses */}
<div className="cp-card p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">
<h2 className="text-sm font-semibold text-[var(--text)]">
آدرسهای مطب ({formatNumber(addresses.length)})
</h2>
{!isReadOnly && (
@@ -1571,7 +1571,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
))}
</div>
) : (
<div className="text-center py-8 text-slate-400 dark:text-slate-500">
<div className="text-center py-8 text-[var(--text-3)]">
<MapPinIcon className="w-10 h-10 mx-auto mb-2 opacity-30" />
<p className="text-sm">هنوز آدرسی ثبت نشده است</p>
{!isReadOnly && (
@@ -1590,18 +1590,18 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
{doctor.clinics && doctor.clinics.length > 0 && (
<div className="cp-card p-6">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">
<h2 className="text-sm font-semibold text-[var(--text)] mb-3">
کلینیکهای مرتبط ({formatNumber(doctor.clinics.length)})
</h2>
<div className="space-y-2">
{doctor.clinics.map(c => (
<div key={c.uuid} className="flex items-start gap-3 p-3 rounded-xl bg-slate-50 dark:bg-gray-800/60">
<div className="w-8 h-8 rounded-lg bg-white dark:bg-gray-700 shadow-sm flex items-center justify-center shrink-0">
<BuildingOfficeIcon className="w-4 h-4 text-slate-400" />
<div key={c.uuid} className="flex items-start gap-3 p-3 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60">
<div className="w-8 h-8 rounded-lg bg-[var(--surface)] shadow-sm flex items-center justify-center shrink-0">
<BuildingOfficeIcon className="w-4 h-4 text-[var(--text-3)]" />
</div>
<div>
<p className="text-sm font-medium text-slate-800 dark:text-slate-200">{c.name}</p>
{c.address && <p className="text-xs text-slate-500 mt-0.5 flex items-center gap-1"><MapPinIcon className="w-3 h-3" />{c.address}</p>}
<p className="text-sm font-medium text-[var(--text)]">{c.name}</p>
{c.address && <p className="text-xs text-[var(--text-2)] mt-0.5 flex items-center gap-1"><MapPinIcon className="w-3 h-3" />{c.address}</p>}
</div>
</div>
))}
@@ -1613,7 +1613,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
{/* Sidebar */}
<div className="space-y-5">
<div className="cp-card p-5">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">
<h2 className="text-sm font-semibold text-[var(--text)] mb-3">
تخصصها ({formatNumber(doctor.specialties.length)})
</h2>
{doctor.specialties.length > 0
@@ -1622,7 +1622,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
<span key={s.id} className={`text-xs font-medium px-2.5 py-1 rounded-lg ${SPECIALTY_COLORS[i % SPECIALTY_COLORS.length]}`}>{s.name}</span>
))}
</div>
: <p className="text-sm text-slate-400">تخصصی ثبت نشده</p>
: <p className="text-sm text-[var(--text-3)]">تخصصی ثبت نشده</p>
}
{!isReadOnly && (
<button onClick={() => 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
</div>
<div className="cp-card p-5">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">
<h2 className="text-sm font-semibold text-[var(--text)] mb-3">
خدمات ({formatNumber(doctor.expertise.length)})
</h2>
{doctor.expertise.length > 0
? <div className="flex flex-wrap gap-1.5">
{doctor.expertise.map(e => (
<span key={e.id} className="text-xs px-2.5 py-1 rounded-lg bg-slate-100 dark:bg-gray-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-gray-700">{e.name}</span>
<span key={e.id} className="text-xs px-2.5 py-1 rounded-lg bg-[var(--surface-2)] text-[var(--text-2)] border border-[var(--border)]">{e.name}</span>
))}
</div>
: <p className="text-sm text-slate-400">خدمتی ثبت نشده</p>
: <p className="text-sm text-[var(--text-3)]">خدمتی ثبت نشده</p>
}
{!isReadOnly && (
<button onClick={() => 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
</div>
<div className="cp-card p-5">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">امتیاز و رضایت</h2>
<h2 className="text-sm font-semibold text-[var(--text)] mb-3">امتیاز و رضایت</h2>
<div className="space-y-3">
<div>
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">امتیاز کلی</p>
<p className="text-xs text-[var(--text-2)] mb-1">امتیاز کلی</p>
<div dir="ltr"><StarRating rate={rateNum} /></div>
</div>
<div>
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1">درصد رضایت</p>
<p className="text-xs text-[var(--text-2)] mb-1">درصد رضایت</p>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 rounded-full bg-slate-200 dark:bg-gray-700">
<div className="flex-1 h-2 rounded-full bg-[var(--surface-3)]">
<div className="h-full rounded-full bg-amber-400 transition-all" style={{ width: `${doctor.satisfaction}%` }} />
</div>
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">{doctor.satisfaction}%</span>
<span className="text-sm font-medium text-[var(--text)]">{doctor.satisfaction}%</span>
</div>
</div>
</div>
</div>
<div className="cp-card p-5">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">UUID</h2>
<h2 className="text-sm font-semibold text-[var(--text)] mb-2">UUID</h2>
<div className="flex items-center gap-2">
<p className="text-[11px] font-mono text-slate-500 dark:text-slate-400 break-all flex-1" dir="ltr">{doctor.uuid}</p>
<p className="text-[11px] font-mono text-[var(--text-2)] break-all flex-1" dir="ltr">{doctor.uuid}</p>
<button onClick={() => { 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">
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
</button>
</div>
@@ -1887,7 +1887,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
</>
}
>
<label className="block text-sm text-slate-600 mb-2">انتخاب نماینده</label>
<label className="block text-sm text-[var(--text-2)] mb-2">انتخاب نماینده</label>
<GlobalSearchableSelect
options={repOptions}
value={selRepId}
+1 -1
View File
@@ -429,7 +429,7 @@ export default function DoctorFormPage() {
<button type="submit" disabled={createMut.isPending} className="cp-btn-primary" style={{ minWidth: 140 }}>
{createMut.isPending ? (
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ width: 16, height: 16, border: '2px solid rgba(255,255,255,.3)', borderTopColor: '#fff', borderRadius: '50%', animation: 'spin .7s linear infinite', flexShrink: 0 }} />
<span style={{ width: 16, height: 16, border: '2px solid rgba(255,255,255,.3)', borderTopColor: 'var(--surface)', borderRadius: '50%', animation: 'spin .7s linear infinite', flexShrink: 0 }} />
در حال ذخیره...
</span>
) : (
+4 -4
View File
@@ -42,20 +42,20 @@ export default function EditSessionPage() {
return (
<div className="fade-in" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 30 }}>
<div className="dark:text-[#A1A1A1]" style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#6B7280', fontSize: 12, padding: '0 16px' }}>
<div onClick={() => nav(-1)} className="bg-white dark:bg-[#222433]" style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-2)', fontSize: 12, padding: '0 16px' }}>
<div onClick={() => nav(-1)} className="bg-[var(--surface)]" style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer' }}>
<ArrowLeftPH style={{ width: 18, height: 18, rotate: '180deg' }} />
<span>بازگشت</span>
</div>
<ArrowLeftD />
<span>پرونده</span>
<ArrowLeftD />
<span className="dark:text-[#D7D8ED]" style={{ color: '#111827' }}>{patientName}</span>
<span style={{ color: 'var(--text)' }}>{patientName}</span>
</div>
</div>
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
<div className="bg-white dark:bg-[#222433]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
<div className="bg-[var(--surface)]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<h2 style={{ fontSize: 16, fontWeight: 700 }}>ویرایش مراجعه</h2>
<button type="button" aria-label="بستن" onClick={() => nav(-1)}
+1 -1
View File
@@ -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' ? 'رمز عبور' : 'ورود با پیامک'}
</button>
+9 -9
View File
@@ -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}
</span>
{hasUnpaid ? (
<span style={{ background: "rgba(255,192,81,0.15)", color: "#f59e0b", fontSize: 14, fontWeight: 500, padding: "4px 12px", borderRadius: 8 }}>
<span style={{ background: "rgba(255,192,81,0.15)", color: "var(--warning)", fontSize: 14, fontWeight: 500, padding: "4px 12px", borderRadius: 8 }}>
تکمیل نشده
</span>
) : (
<span style={{ background: "rgba(60,154,79,0.15)", color: "#3C9A4F", fontSize: 14, fontWeight: 500, padding: "4px 12px", borderRadius: 8 }}>
<span style={{ background: "rgba(60,154,79,0.15)", color: "var(--success)", fontSize: 14, fontWeight: 500, padding: "4px 12px", borderRadius: 8 }}>
تکمیل شده
</span>
)}
@@ -1089,12 +1089,12 @@ function MyPatientsPageInner() {
{/* وسط: شماره تماس و تاریخ تشکیل پرونده */}
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
<BannerInfoRow
icon={<PhoneArrowUpRightIcon style={{ width: 20, color: "#F17732" }} />}
icon={<PhoneArrowUpRightIcon style={{ width: 20, color: "var(--accent)" }} />}
label="شماره تماس:"
value={<span dir="ltr">{selectedPatientPhone}</span>}
/>
<BannerInfoRow
icon={<CalendarDaysIcon style={{ width: 20, color: "#F17732" }} />}
icon={<CalendarDaysIcon style={{ width: 20, color: "var(--accent)" }} />}
label="تاریخ تشکیل پرونده:"
value={formatDate(selectedRecord.created_at)}
/>
@@ -1115,7 +1115,7 @@ function MyPatientsPageInner() {
<button
onClick={() => 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",
}}
>
<UserIcon style={{ width: size * 0.6, height: size * 0.6 }} />
@@ -1882,8 +1882,8 @@ function VisitSummaryCard({
}}
>
{paid
? <CheckCircleIcon style={{ width: 20, color: "#3C9A4F" }} />
: <ClockIcon style={{ width: 20, color: "#F17732" }} />}
? <CheckCircleIcon style={{ width: 20, color: "var(--success)" }} />
: <ClockIcon style={{ width: 20, color: "var(--accent)" }} />}
</div>
</div>
+1 -1
View File
@@ -27,7 +27,7 @@ function Avatar({ name }: { name: string | null }) {
<div style={{
width: 44, height: 44, borderRadius: '50%', flexShrink: 0,
background: 'linear-gradient(145deg, var(--primary), var(--primary-700, var(--primary)))',
display: 'grid', placeItems: 'center', color: '#fff', fontSize: 17, fontWeight: 700,
display: 'grid', placeItems: 'center', color: 'var(--on-primary)', fontSize: 17, fontWeight: 700,
}}>
{(name ?? '؟').charAt(0)}
</div>
+1 -1
View File
@@ -47,7 +47,7 @@ function Avatar({ name }: { name: string | null }) {
<div style={{
width: 32, height: 32, borderRadius: '50%', flexShrink: 0,
background: 'linear-gradient(145deg, var(--primary), var(--primary-700, var(--primary)))',
display: 'grid', placeItems: 'center', color: '#fff', fontSize: 13, fontWeight: 700,
display: 'grid', placeItems: 'center', color: 'var(--on-primary)', fontSize: 13, fontWeight: 700,
}}>
{(name ?? '؟').charAt(0)}
</div>
+73 -73
View File
@@ -16,23 +16,23 @@ import type { Secretary, SecretaryPermissions } from "../types";
const PlusIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M5 10H15" stroke="#EFEFEF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M10 15V5" stroke="#EFEFEF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5 10H15" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--on-primary)' }}/>
<path d="M10 15V5" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--on-primary)' }}/>
</svg>
);
const EyeIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M12.9833 9.99993C12.9833 11.6499 11.6499 12.9833 9.99993 12.9833C8.34993 12.9833 7.0166 11.6499 7.0166 9.99993C7.0166 8.34993 8.34993 7.0166 9.99993 7.0166C11.6499 7.0166 12.9833 8.34993 12.9833 9.99993Z" stroke="#616161" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M9.99987 16.8918C12.9415 16.8918 15.6832 15.1584 17.5915 12.1584C18.3415 10.9834 18.3415 9.00843 17.5915 7.83343C15.6832 4.83343 12.9415 3.1001 9.99987 3.1001C7.0582 3.1001 4.31654 4.83343 2.4082 7.83343C1.6582 9.00843 1.6582 10.9834 2.4082 12.1584C4.31654 15.1584 7.0582 16.8918 9.99987 16.8918Z" stroke="#616161" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12.9833 9.99993C12.9833 11.6499 11.6499 12.9833 9.99993 12.9833C8.34993 12.9833 7.0166 11.6499 7.0166 9.99993C7.0166 8.34993 8.34993 7.0166 9.99993 7.0166C11.6499 7.0166 12.9833 8.34993 12.9833 9.99993Z" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--text-2)' }}/>
<path d="M9.99987 16.8918C12.9415 16.8918 15.6832 15.1584 17.5915 12.1584C18.3415 10.9834 18.3415 9.00843 17.5915 7.83343C15.6832 4.83343 12.9415 3.1001 9.99987 3.1001C7.0582 3.1001 4.31654 4.83343 2.4082 7.83343C1.6582 9.00843 1.6582 10.9834 2.4082 12.1584C4.31654 15.1584 7.0582 16.8918 9.99987 16.8918Z" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--text-2)' }}/>
</svg>
);
const EditIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M11.0504 3.00002L4.20878 10.2417C3.95045 10.5167 3.70045 11.0584 3.65045 11.4334L3.34211 14.1334C3.23378 15.1084 3.93378 15.775 4.90045 15.6084L7.58378 15.15C7.95878 15.0834 8.48378 14.8084 8.74211 14.525L15.5838 7.28335C16.7671 6.03335 17.3004 4.60835 15.4588 2.86668C13.6254 1.14168 12.2338 1.75002 11.0504 3.00002Z" stroke="#616161" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M9.9082 4.2085C10.2665 6.5085 12.1332 8.26683 14.4499 8.50016" stroke="#616161" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M2.5 18.3335H17.5" stroke="#616161" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.0504 3.00002L4.20878 10.2417C3.95045 10.5167 3.70045 11.0584 3.65045 11.4334L3.34211 14.1334C3.23378 15.1084 3.93378 15.775 4.90045 15.6084L7.58378 15.15C7.95878 15.0834 8.48378 14.8084 8.74211 14.525L15.5838 7.28335C16.7671 6.03335 17.3004 4.60835 15.4588 2.86668C13.6254 1.14168 12.2338 1.75002 11.0504 3.00002Z" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--text-2)' }}/>
<path d="M9.9082 4.2085C10.2665 6.5085 12.1332 8.26683 14.4499 8.50016" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--text-2)' }}/>
<path d="M2.5 18.3335H17.5" strokeWidth="1.5" strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" style={{ stroke: 'var(--text-2)' }}/>
</svg>
);
@@ -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)" }}
>
<path d="M6 9L12 15L18 9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
@@ -56,7 +56,7 @@ function Avatar({ name, size = 32 }: { name?: string; size?: number }) {
const firstLetter = (name?.trim().charAt(0) || "?").toUpperCase();
return (
<div
className="rounded-full flex items-center justify-center bg-gradient-to-br from-[#5559CE] to-[#7B7FE8] text-white font-bold flex-shrink-0"
className="rounded-full flex items-center justify-center bg-gradient-to-br from-[var(--primary)] to-[var(--primary)] text-[var(--on-primary)] font-bold flex-shrink-0"
style={{ width: size, height: size, fontSize: size / 2 }}
>
{firstLetter}
@@ -268,7 +268,7 @@ function PermissionAccordions({
return (
<div className="w-full">
<p className="text-[#525252] dark:text-[#D7D8ED] text-[16px] font-bold mb-[16px]">
<p className="text-[var(--text)] text-[16px] font-bold mb-[16px]">
مجوزهای دسترسی
</p>
<div className="flex flex-col gap-[8px] w-full items-stretch">
@@ -278,14 +278,14 @@ function PermissionAccordions({
return (
<div
key={section.key}
className="w-full border border-[#DBDBDB] dark:border-[#343645] rounded-[8px] overflow-hidden"
className="w-full border border-[var(--border-2)] rounded-[8px] overflow-hidden"
>
<button
type="button"
onClick={() => toggleOpen(section.key)}
className="w-full flex items-center justify-between px-[16px] min-h-[64px] cursor-pointer"
>
<p className="text-[#525252] dark:text-[#D7D8ED] text-[14px] font-medium">
<p className="text-[var(--text)] text-[14px] font-medium">
{section.title}
</p>
<ChevronDown open={open} />
@@ -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" }}
/>
<span className="text-[#616161] dark:text-[#A1A1A1] text-[14px]">
<span className="text-[var(--text-2)] text-[14px]">
{item.label}
</span>
</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 (
<textarea
@@ -493,7 +493,7 @@ function SecretaryModal({
<>
<button
onClick={onClose}
className="border border-[#5559CE] dark:border-transparent dark:bg-[#C7CEF4] text-[#5559CE] dark:text-[#222433]
className="border border-[var(--primary)] dark:border-transparent dark:bg-[var(--primary-soft2)] text-[var(--primary)] dark:text-[var(--surface)]
h-[40px] md:h-[44px] lg:h-[48px] py-[11px] px-[48px] rounded-[4px] cursor-pointer"
>
{mode === "view" ? "بستن" : "انصراف"}
@@ -502,7 +502,7 @@ function SecretaryModal({
<button
onClick={handleSubmit}
disabled={saving}
className="bg-[#5559CE] text-[#EFEFEF] rounded-[4px] h-[40px] md:h-[44px] lg:h-[48px] py-[11px] px-[48px] cursor-pointer disabled:opacity-60"
className="bg-[var(--primary)] text-[var(--on-primary)] rounded-[4px] h-[40px] md:h-[44px] lg:h-[48px] py-[11px] px-[48px] cursor-pointer disabled:opacity-60"
>
{saving ? "در حال ذخیره..." : "ذخیره"}
</button>
@@ -515,12 +515,12 @@ function SecretaryModal({
{showDoctorPicker && (
<div className="w-full">
<div className="flex items-center justify-between mb-[16px]">
<p className="text-[#525252] dark:text-[#D7D8ED] text-[16px] font-bold">
<p className="text-[var(--text)] text-[16px] font-bold">
پزشکانِ این منشی
</p>
<span className="text-[13px] text-[#7E7E7E]">{doctorUuids.length} انتخابشده</span>
<span className="text-[13px] text-[var(--text-2)]">{doctorUuids.length} انتخابشده</span>
</div>
<p className="text-[13px] text-[#7E7E7E] mb-[12px]">
<p className="text-[13px] text-[var(--text-2)] mb-[12px]">
منشی فقط به نوبتها و اطلاعاتِ پزشکانِ انتخابشده دسترسی خواهد داشت.
</p>
<DoctorMultiSelect doctors={clinicDoctors} selected={doctorUuids} onChange={setDoctorUuids} />
@@ -529,16 +529,16 @@ function SecretaryModal({
{/* اطلاعات پایه */}
<div className="w-full">
<p className="text-[#525252] dark:text-[#D7D8ED] text-[16px] font-bold mb-[16px]">
<p className="text-[var(--text)] text-[16px] font-bold mb-[16px]">
اطلاعات پایه
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-[16px]">
<div className="flex w-full flex-col items-start justify-start gap-[12px]">
<p className="w-full text-right text-[#525252] dark:text-[#D7D8ED] text-[14px] font-medium">نام</p>
<p className="w-full text-right text-[var(--text)] text-[14px] font-medium">نام</p>
<DefaultTextField placeholder="نام" value={form.name} onChange={(v) => setField("name", v)} disabled={disabled} />
</div>
<div className="flex w-full flex-col items-start justify-start gap-[12px]">
<p className="w-full text-right text-[#525252] dark:text-[#D7D8ED] text-[14px] font-medium">نام خانوادگی</p>
<p className="w-full text-right text-[var(--text)] text-[14px] font-medium">نام خانوادگی</p>
<DefaultTextField placeholder="نام خانوادگی" value={form.family} onChange={(v) => setField("family", v)} disabled={disabled} />
</div>
</div>
@@ -546,17 +546,17 @@ function SecretaryModal({
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-[16px]">
<div className="flex w-full flex-col items-start justify-start gap-[12px]">
<p className="w-full text-right text-[#525252] dark:text-[#D7D8ED] text-[14px] font-medium">شماره موبایل</p>
<p className="w-full text-right text-[var(--text)] text-[14px] font-medium">شماره موبایل</p>
<DefaultTextField numeric maxDigits={11} placeholder="09121234567" value={form.telephone} onChange={(v) => setField("telephone", v)} disabled={disabled || mode === "edit"} />
</div>
<div className="flex w-full flex-col items-start justify-start gap-[12px]">
<p className="w-full text-right text-[#525252] dark:text-[#D7D8ED] text-[14px] font-medium">کد ملی</p>
<p className="w-full text-right text-[var(--text)] text-[14px] font-medium">کد ملی</p>
<DefaultTextField numeric maxDigits={10} placeholder="کد ملی" value={form.national_code} onChange={(v) => setField("national_code", v)} disabled={disabled} />
</div>
</div>
<div className="flex w-full flex-col items-start justify-start gap-[12px]">
<p className="w-full text-right text-[#525252] dark:text-[#D7D8ED] text-[14px] font-medium">آدرس</p>
<p className="w-full text-right text-[var(--text)] text-[14px] font-medium">آدرس</p>
<DefaultTextField placeholder="آدرس" value={form.address} onChange={(v) => setField("address", v)} disabled={disabled} multiline rows={2} />
</div>
@@ -636,15 +636,15 @@ function SecretaryTable({
showDoctor: boolean;
}) {
const headCls =
"text-[#616161] dark:text-[#D7D8ED] text-[14px] font-normal py-[10px] px-[18px] text-start";
"text-[var(--text-2)] dark:text-[var(--text)] text-[14px] font-normal py-[10px] px-[18px] text-start";
const cellCls =
"text-[#616161] dark:text-[#A1A1A1] text-[16px] font-medium px-[18px] py-[10px] whitespace-nowrap";
"text-[var(--text-2)] text-[16px] font-medium px-[18px] py-[10px] whitespace-nowrap";
return (
<div className="hidden lg:block mt-[24px]">
<div className="rounded-[8px] overflow-hidden border border-solid border-[#E7E7E7] dark:border-[#35343D]">
<div className="rounded-[8px] overflow-hidden border border-solid border-[var(--border)]">
<table className="w-full border-collapse">
<thead>
<tr className="bg-[#EFEFEF] dark:bg-[#35343D]">
<tr className="bg-[var(--surface-2)]">
<th className={headCls}>ردیف</th>
<th className={headCls}>نام منشی</th>
{showDoctor && <th className={headCls}>پزشک</th>}
@@ -659,7 +659,7 @@ function SecretaryTable({
{data.map((group, idx) => {
const row = group.primary;
return (
<tr key={group.key} className="border-b border-[#DBDBDB] dark:border-[#343645]">
<tr key={group.key} className="border-b border-[var(--border-2)]">
<td className={cellCls}>{idx + 1}</td>
<td className={cellCls}>
<div className="flex items-center justify-start gap-[8px]">
@@ -682,14 +682,14 @@ function SecretaryTable({
{row.is_active ? (
<button
onClick={() => onDeactivate(group)}
className="border border-[#E53935] text-[#E53935] hover:bg-[#FFEBEE] text-[12px] font-medium py-[6px] px-[16px] rounded-[4px] cursor-pointer"
className="border border-[var(--danger)] text-[var(--danger)] hover:bg-[var(--danger-bg)] text-[12px] font-medium py-[6px] px-[16px] rounded-[4px] cursor-pointer"
>
لغو همکاری
</button>
) : (
<button
onClick={() => onDeactivate(group)}
className="border border-[#2E7D32] text-[#2E7D32] hover:bg-[#E8F5E9] text-[12px] font-medium py-[6px] px-[16px] rounded-[4px] cursor-pointer"
className="border border-[var(--success)] text-[var(--success)] hover:bg-[var(--success-bg)] text-[12px] font-medium py-[6px] px-[16px] rounded-[4px] cursor-pointer"
>
فعالسازی
</button>
@@ -727,49 +727,49 @@ function SecretaryCards({
return (
<div
key={group.key}
className="bg-[#FFF] dark:bg-[#222433] dark:shadow-transparent rounded-[8px] shadow-[0px_1px_24.8px_0px_rgba(204,204,204,0.18)] p-[12px]"
className="bg-[var(--surface)] dark:shadow-transparent rounded-[8px] shadow-[0px_1px_24.8px_0px_rgba(204,204,204,0.18)] p-[12px]"
>
<div className="flex items-center justify-start gap-[8px]">
<Avatar name={item.user_name} size={32} />
<p className="text-[#616161] dark:text-[#D7D8ED] text-[14px] font-medium">{item.user_name}</p>
<p className="text-[var(--text-2)] text-[14px] font-medium">{item.user_name}</p>
</div>
<div className="w-full mt-[16px]">
{showDoctor && (
<>
<div className="flex items-start justify-between gap-[8px]">
<p className="text-[#7E7E7E] dark:text-[#A1A1A1] text-[14px] font-normal shrink-0">پزشک:</p>
<p className="text-[#616161] dark:text-[#A1A1A1] text-[14px] font-normal text-left">
<p className="text-[var(--text-2)] text-[14px] font-normal shrink-0">پزشک:</p>
<p className="text-[var(--text-2)] text-[14px] font-normal text-left">
{group.doctorNames.join("، ")}
</p>
</div>
<span className="block w-full h-px bg-[#EFEFEF] dark:bg-[#343645] my-[8px]" />
<span className="block w-full h-px bg-[var(--surface-2)] my-[8px]" />
</>
)}
<div className="flex items-center justify-between">
<p className="text-[#7E7E7E] dark:text-[#A1A1A1] text-[14px] font-normal">کدملی:</p>
<p className="text-[#616161] dark:text-[#A1A1A1] text-[14px] font-normal">{item.national_code || "-"}</p>
<p className="text-[var(--text-2)] text-[14px] font-normal">کدملی:</p>
<p className="text-[var(--text-2)] text-[14px] font-normal">{item.national_code || "-"}</p>
</div>
<span className="block w-full h-px bg-[#EFEFEF] dark:bg-[#343645] my-[8px]" />
<span className="block w-full h-px bg-[var(--surface-2)] my-[8px]" />
<div className="flex items-center justify-between">
<p className="text-[#7E7E7E] dark:text-[#A1A1A1] text-[14px] font-normal">تاریخ همکاری:</p>
<p className="text-[#616161] dark:text-[#A1A1A1] text-[14px] font-normal">{formatDate(Number(item.created_at))}</p>
<p className="text-[var(--text-2)] text-[14px] font-normal">تاریخ همکاری:</p>
<p className="text-[var(--text-2)] text-[14px] font-normal">{formatDate(Number(item.created_at))}</p>
</div>
<span className="block w-full h-px bg-[#EFEFEF] dark:bg-[#343645] my-[8px]" />
<span className="block w-full h-px bg-[var(--surface-2)] my-[8px]" />
<div className="flex items-center justify-between">
<p className="text-[#7E7E7E] dark:text-[#A1A1A1] text-[14px] font-normal">شماره تماس:</p>
<p className="text-[#616161] dark:text-[#A1A1A1] text-[14px] font-normal" dir="ltr">{item.mobile_number}</p>
<p className="text-[var(--text-2)] text-[14px] font-normal">شماره تماس:</p>
<p className="text-[var(--text-2)] text-[14px] font-normal" dir="ltr">{item.mobile_number}</p>
</div>
</div>
<div className="flex items-center justify-between mt-[16px]">
<button
onClick={() => onView(group)}
className="text-[#5559CE] dark:bg-[#C7CEF4] gap-[5px] text-[14px] font-medium border border-[#5559CE] py-[8px] px-[12px] rounded-[4px] cursor-pointer flex items-center"
className="text-[var(--primary)] dark:bg-[var(--primary-soft2)] gap-[5px] text-[14px] font-medium border border-[var(--primary)] py-[8px] px-[12px] rounded-[4px] cursor-pointer flex items-center"
>
<EyeIcon />
</button>
<button
onClick={() => onEdit(group)}
className="border border-[#EFEFEF] dark:border-[#343645] rounded-[4px] p-[7px] cursor-pointer"
className="border border-[var(--border)] rounded-[4px] p-[7px] cursor-pointer"
>
<EditIcon />
</button>
@@ -778,8 +778,8 @@ function SecretaryCards({
onClick={() => onDeactivate(group)}
className={
item.is_active
? "w-full border border-[#E53935] text-[#E53935] hover:bg-[#FFEBEE] text-[14px] font-medium py-[8px] mt-[12px] rounded-[4px] cursor-pointer"
: "w-full border border-[#2E7D32] text-[#2E7D32] hover:bg-[#E8F5E9] text-[14px] font-medium py-[8px] mt-[12px] rounded-[4px] cursor-pointer"
? "w-full border border-[var(--danger)] text-[var(--danger)] hover:bg-[var(--danger-bg)] text-[14px] font-medium py-[8px] mt-[12px] rounded-[4px] cursor-pointer"
: "w-full border border-[var(--success)] text-[var(--success)] hover:bg-[var(--success-bg)] text-[14px] font-medium py-[8px] mt-[12px] rounded-[4px] cursor-pointer"
}
>
{item.is_active ? "لغو همکاری" : "فعال‌سازی"}
@@ -816,22 +816,22 @@ function DoctorMultiSelect({
const toggleAll = () => onChange(allSelected ? [] : doctors.map((d) => d.uuid));
return (
<div className="w-full border border-[#EFEFEF] dark:border-[#343645] rounded-[8px] overflow-hidden">
<div className="w-full border border-[var(--border)] rounded-[8px] overflow-hidden">
{/* هدر: جستجو + انتخاب همه */}
<div className="flex items-center gap-[8px] p-[10px] border-b border-[#EFEFEF] dark:border-[#343645] bg-[#FAFAFC] dark:bg-[#222433]">
<div className="flex items-center gap-[8px] p-[10px] border-b border-[var(--border)] bg-[var(--surface)]">
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" className="flex-shrink-0">
<path d="M9 16A7 7 0 109 2a7 7 0 000 14zM18 18l-3.5-3.5" stroke="#9A9AB0" strokeWidth="1.5" strokeLinecap="round" />
<path d="M9 16A7 7 0 109 2a7 7 0 000 14zM18 18l-3.5-3.5" style={{ stroke: 'var(--text-3)' }} strokeWidth="1.5" strokeLinecap="round" />
</svg>
<input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="جستجوی پزشک..."
className="flex-1 text-[13px] bg-transparent outline-none text-[#525252] dark:text-[#D7D8ED]"
className="flex-1 text-[13px] bg-transparent outline-none text-[var(--text)]"
/>
<button
type="button"
onClick={toggleAll}
className="text-[12px] text-[#5559CE] font-medium whitespace-nowrap cursor-pointer"
className="text-[12px] text-[var(--primary)] font-medium whitespace-nowrap cursor-pointer"
>
{allSelected ? "لغو همه" : "انتخاب همه"}
</button>
@@ -839,7 +839,7 @@ function DoctorMultiSelect({
{/* چیپ‌های انتخاب‌شده */}
{selected.length > 0 && (
<div className="flex flex-wrap gap-[6px] p-[10px] border-b border-[#EFEFEF] dark:border-[#343645]">
<div className="flex flex-wrap gap-[6px] p-[10px] border-b border-[var(--border)]">
{selected.map((uuid) => {
const d = doctors.find((x) => x.uuid === uuid);
if (!d) return null;
@@ -847,7 +847,7 @@ function DoctorMultiSelect({
<span
key={uuid}
onClick={() => toggle(uuid)}
className="inline-flex items-center gap-[4px] bg-[#EEF0FF] dark:bg-[#33365A] text-[#5559CE] dark:text-[#C7CEF4] text-[12px] px-[8px] py-[3px] rounded-full cursor-pointer"
className="inline-flex items-center gap-[4px] bg-[var(--primary-soft)] text-[var(--primary)] text-[12px] px-[8px] py-[3px] rounded-full cursor-pointer"
>
{d.name}
<span className="text-[14px] leading-none">×</span>
@@ -860,7 +860,7 @@ function DoctorMultiSelect({
{/* لیست پزشکان */}
<div className="max-h-[220px] overflow-y-auto">
{filtered.length === 0 ? (
<p className="text-center text-[12px] text-[#7E7E7E] py-[14px]">نتیجهای یافت نشد</p>
<p className="text-center text-[12px] text-[var(--text-2)] py-[14px]">نتیجهای یافت نشد</p>
) : (
filtered.map((d) => {
const checked = selected.includes(d.uuid);
@@ -868,18 +868,18 @@ function DoctorMultiSelect({
<label
key={d.uuid}
className={
"flex items-center gap-[10px] px-[12px] py-[9px] cursor-pointer border-b border-[#F2F2F6] dark:border-[#2A2C3A] last:border-b-0 " +
(checked ? "bg-[#F5F6FF] dark:bg-[#2A2D45]" : "hover:bg-[#FAFAFC] dark:hover:bg-[#2A2C3A]")
"flex items-center gap-[10px] px-[12px] py-[9px] cursor-pointer border-b border-[var(--border)] last:border-b-0 " +
(checked ? "bg-[var(--primary-soft)] dark:bg-[var(--surface-3)]" : "hover:bg-[var(--surface)] dark:hover:bg-[var(--surface-2)]")
}
>
<input
type="checkbox"
className="accent-[#5559CE] w-[16px] h-[16px]"
className="accent-[var(--primary)] w-[16px] h-[16px]"
checked={checked}
onChange={() => toggle(d.uuid)}
/>
<Avatar name={d.name} size={26} />
<span className="text-[13px] text-[#525252] dark:text-[#D7D8ED]">{d.name}</span>
<span className="text-[13px] text-[var(--text)]">{d.name}</span>
</label>
);
})
@@ -1040,12 +1040,12 @@ function MySecretariesPageContent() {
return (
<div dir="rtl">
<div className="flex items-center justify-between">
<p className="text-[#525252] dark:text-[#D7D8ED] text-[20px] font-bold">لیست منشی ها</p>
<p className="text-[var(--text)] text-[20px] font-bold">لیست منشی ها</p>
</div>
{/* تب‌ها + دکمه افزودن */}
<div className="w-full flex items-end justify-between mt-[20px]">
<div className="flex items-center gap-[8px] border-b border-[#EFEFEF] dark:border-[#343645]">
<div className="flex items-center gap-[8px] border-b border-[var(--border)]">
{["منشی های فعلی", "منشی های قبلی"].map((label, idx) => (
<button
key={idx}
@@ -1053,8 +1053,8 @@ function MySecretariesPageContent() {
className={
"px-[12px] py-[10px] text-[14px] md:text-[16px] font-bold cursor-pointer border-b-2 -mb-px " +
(tab === idx
? "text-[#5559CE] border-[#5559CE]"
: "text-[#495057] dark:text-[#A1A1A1] border-transparent")
? "text-[var(--primary)] border-[var(--primary)]"
: "text-[var(--text)] dark:text-[var(--text-2)] border-transparent")
}
>
{label}
@@ -1063,7 +1063,7 @@ function MySecretariesPageContent() {
</div>
<button
onClick={handleAddClick}
className="shadow-none gap-[8px] bg-[#5559CE] text-[#EFEFEF] text-[14px] md:text-[15px] lg:text-[16px]
className="shadow-none gap-[8px] bg-[var(--primary)] text-[var(--on-primary)] text-[14px] md:text-[15px] lg:text-[16px]
font-medium py-[10px] px-[16px] h-[43px] md:h-[45px] lg:h-[48px] rounded-[4px] cursor-pointer
flex items-center disabled:opacity-60"
>
@@ -1073,11 +1073,11 @@ function MySecretariesPageContent() {
</div>
{!isClinic && !doctorUuid ? (
<div className="mt-[24px] text-center text-[#7E7E7E] py-[40px]">پروفایل پزشک یافت نشد</div>
<div className="mt-[24px] text-center text-[var(--text-2)] py-[40px]">پروفایل پزشک یافت نشد</div>
) : isLoading ? (
<div className="mt-[24px] text-center text-[#7E7E7E] py-[40px]">در حال بارگذاری...</div>
<div className="mt-[24px] text-center text-[var(--text-2)] py-[40px]">در حال بارگذاری...</div>
) : secretaries.length === 0 ? (
<div className="mt-[24px] text-center text-[#7E7E7E] py-[60px]">
<div className="mt-[24px] text-center text-[var(--text-2)] py-[60px]">
{tab === 0 ? "هنوز منشی فعالی اضافه نشده است" : "منشی قبلی‌ای وجود ندارد"}
</div>
) : (
@@ -1101,7 +1101,7 @@ function MySecretariesPageContent() {
{atLimit && (
<div className="mt-[16px]">
<Link to="/admin/subscription" className="text-[#5559CE] text-[13px] underline">
<Link to="/admin/subscription" className="text-[var(--primary)] text-[13px] underline">
برای افزودن منشی بیشتر، پنل خود را ارتقا دهید
</Link>
</div>
+4 -4
View File
@@ -54,10 +54,10 @@ export default function NewSessionPage() {
<div className="fade-in" style={{ width: '100%' }}>
{/* breadcrumb — tauri AddService header */}
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 30 }}>
<div className="dark:text-[#A1A1A1]" style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#6B7280', fontSize: 12, padding: '0 16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-2)', fontSize: 12, padding: '0 16px' }}>
<div
onClick={() => nav(-1)}
className="bg-white dark:bg-[#222433]"
className="bg-[var(--surface)]"
style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer' }}
>
<ArrowLeftPH style={{ width: 18, height: 18, rotate: '180deg' }} />
@@ -66,13 +66,13 @@ export default function NewSessionPage() {
<ArrowLeftD />
<span>پرونده</span>
<ArrowLeftD />
<span className="dark:text-[#D7D8ED]" style={{ color: '#111827' }}>{patientName}</span>
<span style={{ color: 'var(--text)' }}>{patientName}</span>
</div>
</div>
{/* card — tauri width 748 centered */}
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
<div className="bg-white dark:bg-[#222433]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
<div className="bg-[var(--surface)]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
<button
type="button"
+13 -13
View File
@@ -195,10 +195,10 @@ export default function PatientDetailPage() {
display: 'inline-flex', alignItems: 'center', gap: 6, whiteSpace: 'nowrap',
padding: '10px 14px', border: 'none', background: 'none', cursor: 'pointer',
fontFamily: 'inherit', fontSize: 13, fontWeight: on ? 700 : 500,
color: on ? '#5559ce' : '#616161',
borderBottom: `2px solid ${on ? '#5559ce' : 'transparent'}`,
color: on ? 'var(--primary)' : 'var(--text-2)',
borderBottom: `2px solid ${on ? 'var(--primary)' : 'transparent'}`,
}}>
{t.icon(on ? '#5559ce' : '#616161')} {t.label}
{t.icon(on ? 'var(--primary)' : 'var(--text-2)')} {t.label}
</button>
);
})}
@@ -237,8 +237,8 @@ export default function PatientDetailPage() {
<button className={sessionFilter === 'archived' ? 'on' : ''} onClick={() => setSessionFilter('archived')}>آرشیو</button>
</div>
{canUpdate && (
<Link to={`/admin/patients/${uuid}/session/new`} className="flex items-center justify-center gap-2 rounded-[8px]" style={{ height: 48, minWidth: 137, background: '#5559ce', color: '#fff', textDecoration: 'none', padding: '0 16px', fontSize: 14 }}>
<AddTurn color="#fff" /> سرویس جدید
<Link to={`/admin/patients/${uuid}/session/new`} className="flex items-center justify-center gap-2 rounded-[8px]" style={{ height: 48, minWidth: 137, background: 'var(--primary)', color: 'var(--on-primary)', textDecoration: 'none', padding: '0 16px', fontSize: 14 }}>
<AddTurn color="var(--on-primary)" /> سرویس جدید
</Link>
)}
</div>
@@ -485,9 +485,9 @@ interface Note { uuid: string; body: string; pinned: boolean; author: string | n
/** آیکن سنجاق (پین) — پرشده وقتی یادداشت پین است. Heroicons سنجاق ندارد. */
function PinIcon({ filled, color, size = 15 }: { filled?: boolean; color: string; size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill={filled ? color : 'none'} stroke={color} strokeWidth={filled ? 0 : 1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<svg width={size} height={size} viewBox="0 0 24 24" fill={filled ? color : 'none'} stroke="currentColor" strokeWidth={filled ? 0 : 1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" style={{ color }}>
<path d="M9 4h6l-1 5 3 3v2H7v-2l3-3-1-5Z" />
<line x1="12" y1="14" x2="12" y2="21" stroke={color} strokeWidth={1.8} />
<line x1="12" y1="14" x2="12" y2="21" stroke="currentColor" strokeWidth={1.8} />
</svg>
);
}
@@ -988,16 +988,16 @@ function AppointmentsTab({ uuid, q }: {
<div style={{ width: 320, maxWidth: '100%' }}>
<SearchableSelect options={APPT_SORT_OPTS} value={sort} onChange={(v) => setSort(String(v ?? 'newest'))} height={48} />
</div>
<button type="button" aria-label="فیلتر" className="flex items-center justify-center rounded-[4px] cursor-pointer" style={{ width: 62, height: 48, border: '1px solid #5559ce', background: 'transparent' }}>
<TurnsFilter color="#5559ce" />
<button type="button" aria-label="فیلتر" className="flex items-center justify-center rounded-[4px] cursor-pointer" style={{ width: 62, height: 48, border: '1px solid var(--primary)', background: 'transparent' }}>
<TurnsFilter color="var(--primary)" />
</button>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Link to="/admin/appointments/reserve" className="flex items-center justify-center gap-2 rounded-[8px]" style={{ height: 48, minWidth: 137, border: '1px solid #5559ce', color: '#5559ce', textDecoration: 'none', padding: '0 16px', fontSize: 14 }}>
<AddTurn color="#5559ce" /> نوبت رزرو
<Link to="/admin/appointments/reserve" className="flex items-center justify-center gap-2 rounded-[8px]" style={{ height: 48, minWidth: 137, border: '1px solid var(--primary)', color: 'var(--primary)', textDecoration: 'none', padding: '0 16px', fontSize: 14 }}>
<AddTurn color="var(--primary)" /> نوبت رزرو
</Link>
<Link to="/admin/appointments/new" className="flex items-center justify-center gap-2 rounded-[8px]" style={{ height: 48, minWidth: 137, background: '#5559ce', color: '#fff', textDecoration: 'none', padding: '0 16px', fontSize: 14 }}>
<AddTurn color="#fff" /> نوبت جدید
<Link to="/admin/appointments/new" className="flex items-center justify-center gap-2 rounded-[8px]" style={{ height: 48, minWidth: 137, background: 'var(--primary)', color: 'var(--on-primary)', textDecoration: 'none', padding: '0 16px', fontSize: 14 }}>
<AddTurn color="var(--on-primary)" /> نوبت جدید
</Link>
</div>
</div>
+24 -24
View File
@@ -45,15 +45,15 @@ function PatientCard({ r, onView, onEdit, canUpdate }: { r: PatientRecord; onVie
const [menu, setMenu] = useState(false);
return (
<div
className="bg-white dark:bg-[#222433] rounded-[6px] p-[14px] border border-[#EDEDED] dark:border-[#35343D] shadow-sm transition-all duration-300 hover:shadow-md hover:-translate-y-1 cursor-pointer"
className="bg-[var(--surface)] rounded-[6px] p-[14px] border border-[var(--border)] shadow-sm transition-all duration-300 hover:shadow-md hover:-translate-y-1 cursor-pointer"
onClick={(e) => { if ((e.target as HTMLElement).closest('.more-square-icon')) return; onView(); }}
>
<div className="flex items-center justify-between gap-[6px] mb-[12px] pb-[8px] border-b border-[#E0E0E0] dark:border-[#404040]">
<div className="flex items-center justify-between gap-[6px] mb-[12px] pb-[8px] border-b border-[var(--border-2)]">
<div className="flex items-center gap-[6px] flex-1 min-w-0">
<span style={{ width: 28, height: 28, borderRadius: '50%', background: 'var(--primary-soft)', display: 'grid', placeItems: 'center', flexShrink: 0 }}>
<UserIcon style={{ width: 16, color: 'var(--primary)' }} />
</span>
<h3 className="text-[#525252] dark:text-[#D7D8ED] text-[14px] font-semibold truncate">{r.user_name || '—'}</h3>
<h3 className="text-[var(--text)] text-[14px] font-semibold truncate">{r.user_name || '—'}</h3>
</div>
<div className="flex-shrink-0 more-square-icon" style={{ position: 'relative' }}>
<button
@@ -78,20 +78,20 @@ function PatientCard({ r, onView, onEdit, canUpdate }: { r: PatientRecord; onVie
<div className="mb-[12px] my-4" style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div className="flex items-start justify-between text-[15px]">
<span className="text-[#616161] dark:text-[#A1A1A1] font-medium min-w-[65px]">شماره پرونده:</span>
<span className="text-[#525252] dark:text-[#D7D8ED] truncate">{r.record_number || '—'}</span>
<span className="text-[var(--text-2)] font-medium min-w-[65px]">شماره پرونده:</span>
<span className="text-[var(--text)] truncate">{r.record_number || '—'}</span>
</div>
<div className="flex items-start justify-between text-[15px]">
<span className="text-[#616161] dark:text-[#A1A1A1] font-medium min-w-[65px]">موبایل:</span>
<span className="text-[#525252] dark:text-[#D7D8ED] truncate" dir="ltr">{r.user_mobile || '—'}</span>
<span className="text-[var(--text-2)] font-medium min-w-[65px]">موبایل:</span>
<span className="text-[var(--text)] truncate" dir="ltr">{r.user_mobile || '—'}</span>
</div>
</div>
<div
className="flex items-center pt-[8px] justify-end gap-[8px] border-t border-[#E0E0E0] dark:border-[#404040]"
className="flex items-center pt-[8px] justify-end gap-[8px] border-t border-[var(--border-2)]"
onClick={(e) => e.stopPropagation()}
>
<span className="text-[#616161] dark:text-[#A1A1A1] text-[14px] min-w-[65px]">برچسبها:</span>
<span className="text-[var(--text-2)] text-[14px] min-w-[65px]">برچسبها:</span>
<div className="flex-1 flex justify-end"><PatientTagsCell record={r} /></div>
</div>
</div>
@@ -140,12 +140,12 @@ export default function PatientsListPage() {
// نمایش جدولی/کارتی — دو دکمه با آیکون SVG سفارشی (عین tauri ViewModeToggle).
const viewToggle = (
<div className="flex border border-[#E0E0E0] dark:border-[#404040] rounded-[4px] overflow-hidden shrink-0">
<button type="button" aria-label="نمایش جدولی" onClick={() => switchView('table')} style={{ padding: 8, border: 'none', cursor: 'pointer', display: 'grid', placeItems: 'center', background: view === 'table' ? '#f4f5fd' : 'transparent' }}>
<PatientsGridView color={view === 'table' ? '#5559ce' : '#616161'} />
<div className="flex border border-[var(--border-2)] rounded-[4px] overflow-hidden shrink-0">
<button type="button" aria-label="نمایش جدولی" onClick={() => switchView('table')} style={{ padding: 8, border: 'none', cursor: 'pointer', display: 'grid', placeItems: 'center', background: view === 'table' ? 'var(--primary-soft)' : 'transparent' }}>
<PatientsGridView color={view === 'table' ? 'var(--primary)' : 'var(--text-2)'} />
</button>
<button type="button" aria-label="نمایش کارتی" onClick={() => switchView('card')} style={{ padding: 8, border: 'none', cursor: 'pointer', display: 'grid', placeItems: 'center', background: view === 'card' ? '#f4f5fd' : 'transparent' }}>
<PatientsCategoryView color={view === 'card' ? '#5559ce' : '#616161'} />
<button type="button" aria-label="نمایش کارتی" onClick={() => switchView('card')} style={{ padding: 8, border: 'none', cursor: 'pointer', display: 'grid', placeItems: 'center', background: view === 'card' ? 'var(--primary-soft)' : 'transparent' }}>
<PatientsCategoryView color={view === 'card' ? 'var(--primary)' : 'var(--text-2)'} />
</button>
</div>
);
@@ -153,7 +153,7 @@ export default function PatientsListPage() {
return (
<div className="fade-in">
{/* Head — تیتر تنها (tauri files/head) */}
<p className="text-[#525252] dark:text-[#D7D8ED] text-[16px] md:text-[18px] lg:text-[20px] font-bold">پروندهها</p>
<p className="text-[var(--text)] text-[16px] md:text-[18px] lg:text-[20px] font-bold">پروندهها</p>
{/* Inputs — ردیف کنترل‌ها (tauri files/inputs) */}
<div className="my-[16px] md:my-[20px] lg:my-[24px]">
@@ -161,16 +161,16 @@ export default function PatientsListPage() {
{/* جستجو + سوییچ نما */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 flex-1 w-full">
<div className="w-full md:w-[442px]">
<div className="flex items-center rounded-[6px] border border-[#EFEFEF] dark:border-[#35343D] bg-[#FAFAFA] dark:bg-[#222433]" style={{ height: 48, paddingInline: 4 }}>
<div className="flex items-center rounded-[6px] border border-[var(--border)] bg-[var(--bg)]" style={{ height: 48, paddingInline: 4 }}>
<input
dir="rtl"
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
placeholder="کد ملی، شماره پرونده یا نام مراجعه کننده را وارد کنید..."
className="text-[#525252] dark:text-[#D7D8ED]"
className="text-[var(--text)]"
style={{ flex: 1, minWidth: 0, background: 'transparent', border: 'none', outline: 'none', fontSize: 13, textAlign: 'left' }}
/>
<span style={{ width: 40, height: 40, marginInlineEnd: -4, flexShrink: 0, borderRadius: 6, background: '#5559CE', display: 'grid', placeItems: 'center' }}>
<span style={{ width: 40, height: 40, marginInlineEnd: -4, flexShrink: 0, borderRadius: 6, background: 'var(--primary)', display: 'grid', placeItems: 'center' }}>
<SearchHeaderP color="white" />
</span>
</div>
@@ -183,21 +183,21 @@ export default function PatientsListPage() {
<button
type="button" aria-label="فیلترها" onClick={() => setFilterOpen(true)}
className="flex items-center justify-center rounded-[4px] cursor-pointer"
style={{ width: 62, height: 48, border: '1px solid #5559ce', background: 'transparent', position: 'relative' }}
style={{ width: 62, height: 48, border: '1px solid var(--primary)', background: 'transparent', position: 'relative' }}
>
<TurnsFilter color="#5559ce" />
<TurnsFilter color="var(--primary)" />
{activeFilters > 0 && (
<span style={{ position: 'absolute', top: -6, insetInlineEnd: -6, minWidth: 16, height: 16, padding: '0 4px', borderRadius: 999, background: '#5559ce', color: '#fff', fontSize: 10, display: 'grid', placeItems: 'center' }}>{formatNumber(activeFilters)}</span>
<span style={{ position: 'absolute', top: -6, insetInlineEnd: -6, minWidth: 16, height: 16, padding: '0 4px', borderRadius: 999, background: 'var(--primary)', color: 'var(--on-primary)', fontSize: 10, display: 'grid', placeItems: 'center' }}>{formatNumber(activeFilters)}</span>
)}
</button>
{canCreate && (
<button
type="button" onClick={() => navigate('/admin/patients/new')}
className="flex items-center justify-center gap-2 rounded-[4px] cursor-pointer"
style={{ height: 48, minWidth: 137, background: '#5559ce', border: 'none', padding: '0 16px' }}
style={{ height: 48, minWidth: 137, background: 'var(--primary)', border: 'none', padding: '0 16px' }}
>
<AddTurn color="#fff" />
<span style={{ color: '#fff', fontSize: 14 }}>تشکیل پرونده</span>
<AddTurn color="var(--on-primary)" />
<span style={{ color: 'var(--on-primary)', fontSize: 14 }}>تشکیل پرونده</span>
</button>
)}
</div>
+5 -5
View File
@@ -67,7 +67,7 @@ export default function PaymentDetailPage() {
]}
action={
<button onClick={() => navigate('/admin/payments')}
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">
<ArrowRightIcon className="w-4 h-4" />
بازگشت
</button>
@@ -82,7 +82,7 @@ export default function PaymentDetailPage() {
</div>
) : payment ? (
<div className="max-w-lg">
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6">
<InfoRow label="شناسه" value={<span dir="ltr" className="font-mono text-xs">{payment.uuid}</span>} />
<InfoRow label="شماره سفارش" value={payment.order_id ? <span dir="ltr" className="font-mono text-xs">{payment.order_id}</span> : null} />
<InfoRow label="بیمار" value={payment.patient_name} />
@@ -109,7 +109,7 @@ export default function PaymentDetailPage() {
</div>
{payment.refunds && payment.refunds.length > 0 && (
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6 mt-4">
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6 mt-4">
<h3 className="text-sm font-semibold mb-3">استردادها</h3>
{payment.refunds.map((r, i) => (
<InfoRow
@@ -118,7 +118,7 @@ export default function PaymentDetailPage() {
value={
<span>
{formatRial(r.amount)}
{r.ref && <span dir="ltr" className="font-mono text-xs text-gray-400"> ({r.ref})</span>}
{r.ref && <span dir="ltr" className="font-mono text-xs text-[var(--text-3)]"> ({r.ref})</span>}
</span>
}
/>
@@ -153,7 +153,7 @@ export default function PaymentDetailPage() {
/>
</div>
) : (
<div className="bg-white rounded-2xl border border-gray-100 p-16 text-center text-gray-400">
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] p-16 text-center text-[var(--text-3)]">
پرداختی یافت نشد
</div>
)}
@@ -90,7 +90,7 @@ export default function RepresentationBlogFormPage() {
<div>
<label>عنوان مقاله *</label>
<input {...register('title')} className="cp-input h-11" placeholder="عنوان جذاب بنویسید..." />
{errors.title && <p className="text-red-500 text-xs mt-1">{errors.title.message}</p>}
{errors.title && <p className="text-[var(--danger)] text-xs mt-1">{errors.title.message}</p>}
</div>
<div>
<label>خلاصه</label>
@@ -112,7 +112,7 @@ export default function RepresentationBlogFormPage() {
</div>
)}
/>
{errors.body && <p className="text-red-500 text-xs mt-1">{errors.body.message}</p>}
{errors.body && <p className="text-[var(--danger)] text-xs mt-1">{errors.body.message}</p>}
</div>
</div>
+23 -23
View File
@@ -47,7 +47,7 @@ interface UnassignedDoctor {
function DetailRow({ label, value }: { label: string; value: React.ReactNode }) {
return (
<div className="cp-info-row items-start gap-4">
<span className="text-sm text-gray-500 w-40 shrink-0">{label}</span>
<span className="text-sm text-[var(--text-2)] w-40 shrink-0">{label}</span>
<span className="cp-info-value">{value ?? '—'}</span>
</div>
);
@@ -191,10 +191,10 @@ export default function RepresentationDetailPage() {
{ label: 'جزئیات' },
]}
/>
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6 animate-pulse">
<div className="bg-[var(--surface)] rounded-2xl shadow-sm border border-[var(--border)] p-6 animate-pulse">
<div className="space-y-4">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="h-8 bg-gray-100 rounded" />
<div key={i} className="h-8 bg-[var(--surface-2)] rounded" />
))}
</div>
</div>
@@ -212,7 +212,7 @@ export default function RepresentationDetailPage() {
{ label: 'نمایندگان', to: '/admin/representations' },
]}
/>
<div className="cp-card p-12 text-center text-slate-400 dark:text-slate-500">
<div className="cp-card p-12 text-center text-[var(--text-3)]">
نمایندهای با این شناسه یافت نشد.
</div>
</div>
@@ -237,8 +237,8 @@ export default function RepresentationDetailPage() {
disabled={toggleActiveMutation.isPending}
className={`flex items-center gap-1.5 px-3 py-2 text-sm rounded-[10px] border transition-colors disabled:opacity-50 ${
isActive
? 'border-red-300 text-red-600 hover:bg-red-50'
: 'border-green-300 text-green-600 hover:bg-green-50'
? 'border-[var(--danger)] text-[var(--danger)] hover:bg-[var(--danger-bg)]'
: 'border-[var(--success)] text-[var(--success)] hover:bg-[var(--success-bg)]'
}`}
>
{isActive
@@ -248,14 +248,14 @@ export default function RepresentationDetailPage() {
</button>
<button
onClick={openEdit}
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"
className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-[10px] border border-[var(--border-2)] text-[var(--text)] hover:bg-[var(--surface-2)] transition-colors"
>
<PencilIcon className="w-4 h-4" />
ویرایش
</button>
<button
onClick={() => setDeleteOpen(true)}
className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-[10px] border border-red-300 text-red-600 hover:bg-red-50 transition-colors"
className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-[10px] border border-[var(--danger)] text-[var(--danger)] hover:bg-[var(--danger-bg)] transition-colors"
>
<TrashIcon className="w-4 h-4" />
حذف
@@ -275,7 +275,7 @@ export default function RepresentationDetailPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Basic Info */}
<div className="cp-card p-6">
<h2 className="text-sm font-semibold text-gray-700 mb-4">اطلاعات پایه</h2>
<h2 className="text-sm font-semibold text-[var(--text)] mb-4">اطلاعات پایه</h2>
<DetailRow label="نام کامل" value={rep.full_name} />
<DetailRow label="موبایل" value={
rep.mobile_number
@@ -290,7 +290,7 @@ export default function RepresentationDetailPage() {
{/* Bank Account */}
<div className="cp-card p-6">
<h2 className="text-sm font-semibold text-gray-700 mb-4">اطلاعات بانکی</h2>
<h2 className="text-sm font-semibold text-[var(--text)] mb-4">اطلاعات بانکی</h2>
{rep.bank_account ? (
<>
<DetailRow label="شماره کارت" value={
@@ -306,7 +306,7 @@ export default function RepresentationDetailPage() {
} />
</>
) : (
<p className="text-sm text-gray-400 text-center py-6">اطلاعات بانکی ثبت نشده</p>
<p className="text-sm text-[var(--text-3)] text-center py-6">اطلاعات بانکی ثبت نشده</p>
)}
</div>
</div>
@@ -316,7 +316,7 @@ export default function RepresentationDetailPage() {
<div className="flex items-center justify-between mb-4 flex-wrap gap-2">
<div className="flex items-center gap-2">
<UserGroupIcon className="w-5 h-5 text-[var(--text-3)]" />
<h2 className="text-sm font-semibold text-gray-700">پزشکان این نماینده</h2>
<h2 className="text-sm font-semibold text-[var(--text)]">پزشکان این نماینده</h2>
<span className="badge violet">{formatNumber(repDoctorsTotal)}</span>
</div>
<button className="btn primary sm inline-flex items-center gap-1" onClick={() => setAttachOpen(true)}>
@@ -324,11 +324,11 @@ export default function RepresentationDetailPage() {
</button>
</div>
{doctorsQuery.isLoading ? (
<p className="text-sm text-gray-400 text-center py-8">در حال بارگذاری</p>
<p className="text-sm text-[var(--text-3)] text-center py-8">در حال بارگذاری</p>
) : repDoctors.length === 0 ? (
<div className="text-center py-10">
<UserGroupIcon className="w-10 h-10 mx-auto text-gray-300 mb-2" />
<p className="text-sm text-gray-400">این نماینده هنوز پزشکی ندارد</p>
<UserGroupIcon className="w-10 h-10 mx-auto text-[var(--text-3)] mb-2" />
<p className="text-sm text-[var(--text-3)]">این نماینده هنوز پزشکی ندارد</p>
<button className="btn primary sm mt-3 inline-flex items-center gap-1" onClick={() => setAttachOpen(true)}>
<PlusIcon className="w-4 h-4" /> افزودن اولین پزشک
</button>
@@ -366,7 +366,7 @@ export default function RepresentationDetailPage() {
<div className="flex items-center justify-between mb-4 flex-wrap gap-2">
<div className="flex items-center gap-2">
<CalendarDaysIcon className="w-5 h-5 text-[var(--text-3)]" />
<h2 className="text-sm font-semibold text-gray-700">نوبتهای این نماینده</h2>
<h2 className="text-sm font-semibold text-[var(--text)]">نوبتهای این نماینده</h2>
{!appointmentsQuery.isLoading && <span className="badge blue">{formatNumber(repAppointmentsTotal)}</span>}
</div>
<div className="seg">
@@ -375,11 +375,11 @@ export default function RepresentationDetailPage() {
</div>
</div>
{appointmentsQuery.isLoading ? (
<p className="text-sm text-gray-400 text-center py-8">در حال بارگذاری</p>
<p className="text-sm text-[var(--text-3)] text-center py-8">در حال بارگذاری</p>
) : repAppointments.length === 0 ? (
<div className="text-center py-10">
<CalendarDaysIcon className="w-10 h-10 mx-auto text-gray-300 mb-2" />
<p className="text-sm text-gray-400">
<CalendarDaysIcon className="w-10 h-10 mx-auto text-[var(--text-3)] mb-2" />
<p className="text-sm text-[var(--text-3)]">
{apptScope === 'upcoming' ? 'نوبت آینده‌ای ثبت نشده است' : 'نوبت گذشته‌ای وجود ندارد'}
</p>
</div>
@@ -422,16 +422,16 @@ export default function RepresentationDetailPage() {
onChange={(e) => setAttachSearch(e.target.value)}
/>
{unassignedQuery.isLoading ? (
<p className="text-sm text-gray-400 text-center py-6">در حال بارگذاری</p>
<p className="text-sm text-[var(--text-3)] text-center py-6">در حال بارگذاری</p>
) : unassignedDoctors.length === 0 ? (
<p className="text-sm text-gray-400 text-center py-6">پزشکِ بدون نمایندهای یافت نشد</p>
<p className="text-sm text-[var(--text-3)] text-center py-6">پزشکِ بدون نمایندهای یافت نشد</p>
) : (
<div style={{ maxHeight: 360, overflowY: 'auto' }}>
{unassignedDoctors.map((d) => (
<div key={d.uuid} className="flex items-center justify-between py-2 border-b border-gray-100">
<div key={d.uuid} className="flex items-center justify-between py-2 border-b border-[var(--border)]">
<div>
<b className="text-sm">{d.name}</b>
<div className="text-xs text-gray-400" dir="ltr">{d.medical_code ?? d.mobile ?? '—'}</div>
<div className="text-xs text-[var(--text-3)]" dir="ltr">{d.medical_code ?? d.mobile ?? '—'}</div>
</div>
<button className="btn primary sm" disabled={attachMutation.isPending}
onClick={() => attachMutation.mutate(d.uuid)}>افزودن</button>
+1 -1
View File
@@ -57,7 +57,7 @@ export default function SelectContextPage() {
padding: '.875rem 1rem',
border: '1.5px solid var(--border, #e2e8f0)',
borderRadius: '0.625rem',
background: loading === ctx.db_uuid ? 'var(--surface-alt, #f4f6f9)' : '#fff',
background: loading === ctx.db_uuid ? 'var(--surface-alt, #f4f6f9)' : 'var(--surface)',
cursor: loading !== null ? 'wait' : 'pointer',
textAlign: 'right',
transition: 'border-color .15s, box-shadow .15s',
+4 -4
View File
@@ -57,10 +57,10 @@ export default function SessionPaymentPage() {
<div className="fade-in" style={{ width: '100%' }}>
{/* breadcrumb — tauri AddService header */}
<div style={{ display: 'flex', justifyContent: 'flex-start', marginBottom: 30 }}>
<div className="dark:text-[#A1A1A1]" style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#6B7280', fontSize: 12, padding: '0 16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-2)', fontSize: 12, padding: '0 16px' }}>
<div
onClick={() => nav(-1)}
className="bg-white dark:bg-[#222433]"
className="bg-[var(--surface)]"
style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 8px', borderRadius: 12, cursor: 'pointer' }}
>
<ArrowLeftPH style={{ width: 18, height: 18, rotate: '180deg' }} />
@@ -69,13 +69,13 @@ export default function SessionPaymentPage() {
<ArrowLeftD />
<span>پرونده</span>
<ArrowLeftD />
<span className="dark:text-[#D7D8ED]" style={{ color: '#111827' }}>{patientName}</span>
<span style={{ color: 'var(--text)' }}>{patientName}</span>
</div>
</div>
{/* card — tauri width 748 centered */}
<div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
<div className="bg-white dark:bg-[#222433]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
<div className="bg-[var(--surface)]" style={{ width: 748, maxWidth: '100%', padding: 24 }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
<button
type="button"
+1 -1
View File
@@ -382,7 +382,7 @@ export default function SmsPage() {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
minWidth: 18, height: 18, padding: '0 4px', borderRadius: 9,
fontSize: 10, fontWeight: 700,
background: 'oklch(0.62 0.22 30)', color: '#fff', lineHeight: 1,
background: 'oklch(0.62 0.22 30)', color: 'var(--on-primary)', lineHeight: 1,
}}>
{t.badge}
</span>
+3 -3
View File
@@ -117,7 +117,7 @@ function SmsWalletPageInner() {
<div style={{
background: 'linear-gradient(135deg, oklch(0.52 0.22 256), oklch(0.40 0.18 256))',
borderRadius: 'var(--r)', padding: '24px 24px 20px',
color: '#fff', position: 'relative', overflow: 'hidden', gridColumn: '1 / 2',
color: 'var(--on-primary)', position: 'relative', overflow: 'hidden', gridColumn: '1 / 2',
}}>
<div style={{
position: 'absolute', top: -30, left: -30,
@@ -147,14 +147,14 @@ function SmsWalletPageInner() {
<div style={{
height: '100%', borderRadius: 8,
width: `${Math.min(100, (smsCount / 500) * 100)}%`,
background: isLowBalance ? '#ef4444' : 'rgba(255,255,255,0.85)',
background: isLowBalance ? 'var(--danger)' : 'rgba(255,255,255,0.85)',
transition: 'width 0.4s ease',
}} />
</div>
{canCreate && (
<button
style={{
background: '#fff', color: 'var(--primary)', border: 'none', borderRadius: 8,
background: 'var(--surface)', color: 'var(--primary)', border: 'none', borderRadius: 8,
padding: '8px 18px', fontWeight: 700, fontSize: 13.5, cursor: 'pointer',
display: 'inline-flex', alignItems: 'center', gap: 6,
}}
+12 -12
View File
@@ -44,9 +44,9 @@ const PLAN_META: Record<string, { label: string; desc: string; tint: string }> =
/** Gradient blur behind each plan card — matches the tauri source per plan tier. */
const PLAN_GRADIENT: Record<string, string> = {
free: 'radial-gradient(circle, #f5ecfd 0%, #e9eaf9 70%, transparent 100%)',
basic: 'radial-gradient(circle, #e2eee5 0%, #f5edfd 60%, transparent 100%)',
professional: 'linear-gradient(180deg, #f9eedd 0%, #e8e9fa 0%, rgba(255,255,255,0) 100%)',
free: 'radial-gradient(circle, var(--violet-bg) 0%, var(--primary-soft) 70%, transparent 100%)',
basic: 'radial-gradient(circle, var(--success-bg) 0%, var(--violet-bg) 60%, transparent 100%)',
professional: 'linear-gradient(180deg, var(--warning-bg) 0%, var(--primary-soft) 0%, transparent 100%)',
};
export const planMetaOf = (name: string) => PLAN_META[name] ?? PLAN_META.free;
@@ -305,7 +305,7 @@ function CurrentPlanCard({ my, onRenew }: { my: MySubscriptionData['subscription
{daysLeft < 3 && (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 4 }}>
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<InfoCircleRedFilled color="#d22d32" />
<InfoCircleRedFilled color="var(--danger)" />
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
{daysLeft <= 0 ? 'اشتراک شما به پایان رسیده است' : `${formatNumber(daysLeft)} روز تا پایان اشتراک`}
</span>
@@ -380,7 +380,7 @@ function PlanCard({
data-testid={`plan-card-${plan.name}`}
style={{
width: '100%', minWidth: 0, height: 522, borderRadius: 10,
border: isSelected ? '2px solid #494cb3' : '2px solid var(--border)',
border: isSelected ? '2px solid var(--primary-600)' : '2px solid var(--border)',
background: 'var(--surface)', display: 'flex', flexDirection: 'column',
padding: 16, position: 'relative', overflow: 'hidden',
}}
@@ -397,11 +397,11 @@ function PlanCard({
{isPopular && (
<div style={{
position: 'absolute', width: 'fit-content', height: 32, top: -18, right: -20,
background: '#3d4395', color: '#fff', padding: '0 16px', borderRadius: '0 10px 0 10px',
background: 'var(--primary-700)', color: 'var(--on-primary)', padding: '0 16px', borderRadius: '0 10px 0 10px',
fontSize: 13, fontWeight: 500, display: 'flex', alignItems: 'center', gap: 8, zIndex: 1,
}}>
محبوبترین
<SparkleIcon size={15} color="#FDD835" />
<SparkleIcon size={15} color="var(--warning)" />
</div>
)}
@@ -490,8 +490,8 @@ function PlanCard({
</span>
<span style={{ position: 'relative', display: 'inline-flex', width: 22, height: 22 }}>
{enabled
? <CheckCircleIcon style={{ width: 22, height: 22, color: '#3c9a4f' }} />
: <CloseCircleFilled size={22} color="#d7d7d7" />}
? <CheckCircleIcon style={{ width: 22, height: 22, color: 'var(--success)' }} />
: <CloseCircleFilled size={22} color="var(--border-2)" />}
</span>
</div>
);
@@ -507,7 +507,7 @@ function PlanCard({
}}>
<button
onClick={onTrial} disabled={trialPending}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 16, fontWeight: 500, color: '#5559ce' }}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 16, fontWeight: 500, color: 'var(--primary)' }}
>
{trialPending ? 'در حال فعال‌سازی...' : 'فعال سازی'}
</button>
@@ -519,7 +519,7 @@ function PlanCard({
</div>
</div>
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none' }} viewBox="0 0 268 59" preserveAspectRatio="none">
<rect x="0.5" y="0.5" width="267" height="58" rx="6" fill="transparent" stroke="#636ed0" strokeWidth="1" strokeDasharray="6 4" />
<rect x="0.5" y="0.5" width="267" height="58" rx="6" fill="transparent" strokeWidth="1" strokeDasharray="6 4" style={{ stroke: 'var(--primary)' }}/>
</svg>
</div>
)}
@@ -541,7 +541,7 @@ function PlanCard({
disabled={!selectedPeriod}
style={{
marginTop: 'auto', width: '100%', height: 48, borderRadius: 4, border: 'none',
background: 'var(--primary)', color: '#fff', fontWeight: 700, fontSize: 16,
background: 'var(--primary)', color: 'var(--on-primary)', fontWeight: 700, fontSize: 16,
fontFamily: 'inherit', cursor: selectedPeriod ? 'pointer' : 'not-allowed',
}}
>
+63 -63
View File
@@ -72,10 +72,10 @@ const MEDICAL_SECTIONS: {
const ROLE_META: Record<string, { label: string; cls: string; dot: string }> = {
admin: { label: 'ادمین', cls: 'bg-violet-100 dark:bg-violet-400/10 text-violet-700 dark:text-violet-300', dot: '#8b5cf6' },
doctor: { label: 'پزشک', cls: 'bg-blue-100 dark:bg-blue-400/10 text-blue-700 dark:text-blue-300', dot: '#3b82f6' },
doctor: { label: 'پزشک', cls: 'bg-[var(--info-bg)] dark:bg-blue-400/10 text-[var(--info)] dark:text-blue-300', dot: '#3b82f6' },
secretary: { label: 'منشی', cls: 'bg-orange-100 dark:bg-orange-400/10 text-orange-700 dark:text-orange-300', dot: '#f97316' },
clinic: { label: 'کلینیک', cls: 'bg-emerald-100 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300', dot: '#10b981' },
patient: { label: 'بیمار', cls: 'bg-slate-100 dark:bg-slate-400/10 text-slate-600 dark:text-slate-400', dot: '#94a3b8' },
patient: { label: 'بیمار', cls: 'bg-[var(--surface-2)] dark:bg-slate-400/10 text-[var(--text-2)] dark:text-[var(--text-3)]', dot: '#94a3b8' },
};
const ROLE_MAP: Record<string, string[]> = {
@@ -107,7 +107,7 @@ function BigAvatar({ name, id }: { name: string | null; id: number }) {
? name.split(' ').filter(Boolean).map(w => w[0]).join('').toUpperCase().slice(0, 2)
: '؟';
return (
<div className={`w-20 h-20 rounded-2xl bg-gradient-to-br ${AVATAR_COLORS[id % AVATAR_COLORS.length]} flex items-center justify-center text-white text-2xl font-bold shadow-lg shrink-0`}>
<div className={`w-20 h-20 rounded-2xl bg-gradient-to-br ${AVATAR_COLORS[id % AVATAR_COLORS.length]} flex items-center justify-center text-[var(--on-primary)] text-2xl font-bold shadow-lg shrink-0`}>
{initials}
</div>
);
@@ -124,21 +124,21 @@ function InfoCard({ icon: Icon, label, value, mono = false, copyable = false }:
}
};
return (
<div className="flex items-start gap-3 p-4 rounded-xl bg-slate-50 dark:bg-gray-800/60 hover:bg-slate-100 dark:hover:bg-gray-800 transition-colors group">
<div className="w-9 h-9 rounded-lg bg-white dark:bg-gray-700 shadow-sm flex items-center justify-center shrink-0">
<Icon className="w-4.5 h-4.5 text-slate-500 dark:text-slate-400" />
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60 hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] transition-colors group">
<div className="w-9 h-9 rounded-lg bg-[var(--surface)] shadow-sm flex items-center justify-center shrink-0">
<Icon className="w-4.5 h-4.5 text-[var(--text-2)]" />
</div>
<div className="flex-1 min-w-0">
<p className="text-[11px] font-medium text-slate-400 dark:text-slate-500 uppercase tracking-wide mb-0.5">{label}</p>
<p className={`text-sm font-medium text-slate-800 dark:text-slate-200 truncate ${mono ? 'font-mono' : ''}`}
<p className="text-[11px] font-medium text-[var(--text-3)] uppercase tracking-wide mb-0.5">{label}</p>
<p className={`text-sm font-medium text-[var(--text)] dark:text-[var(--text)] truncate ${mono ? 'font-mono' : ''}`}
dir={mono ? 'ltr' : undefined}>
{value ?? <span className="text-slate-400 dark:text-slate-500 font-normal">ثبت نشده</span>}
{value ?? <span className="text-[var(--text-3)] font-normal">ثبت نشده</span>}
</p>
</div>
{copyable && typeof value === 'string' && (
<button
onClick={handleCopy}
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 dark:hover:text-slate-300 hover:bg-white dark:hover:bg-gray-700 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)] dark:hover:text-[var(--text-3)] hover:bg-[var(--surface)] dark:hover:bg-[var(--surface-2)] transition-all shrink-0"
>
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
</button>
@@ -181,13 +181,13 @@ function ChangeRoleModal({ current, loading, onSave, onClose }: {
className={`flex items-center gap-3 p-3 rounded-xl cursor-pointer border-2 transition-all ${
selected === key
? 'border-primary-500 bg-primary-50 dark:bg-primary-500/10'
: 'border-transparent hover:bg-slate-50 dark:hover:bg-gray-800 border border-slate-200 dark:border-gray-700'
: 'border-transparent hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] border border-[var(--border)] dark:border-[var(--border)]'
}`}
>
<input type="radio" name="role" value={key} checked={selected === key}
onChange={() => setSelected(key)} className="sr-only" />
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: meta.dot }} />
<span className="text-sm font-medium text-slate-800 dark:text-slate-200 flex-1">{meta.label}</span>
<span className="text-sm font-medium text-[var(--text)] flex-1">{meta.label}</span>
{selected === key && <CheckCircleIcon className="w-4 h-4 text-primary-600 dark:text-primary-400" />}
</label>
))}
@@ -320,11 +320,11 @@ export default function UserDetailPage() {
return (
<div className="animate-slide-up">
<div className="cp-card p-16 flex flex-col items-center gap-4 text-center">
<div className="w-16 h-16 rounded-2xl bg-red-100 dark:bg-red-400/10 flex items-center justify-center">
<XCircleIcon className="w-8 h-8 text-red-500" />
<div className="w-16 h-16 rounded-2xl bg-[var(--danger-bg)] dark:bg-red-400/10 flex items-center justify-center">
<XCircleIcon className="w-8 h-8 text-[var(--danger)]" />
</div>
<p className="font-semibold text-slate-800 dark:text-slate-200">کاربر یافت نشد</p>
<p className="text-sm text-slate-500 dark:text-slate-400">این کاربر حذف شده یا UUID اشتباه است.</p>
<p className="font-semibold text-[var(--text)]">کاربر یافت نشد</p>
<p className="text-sm text-[var(--text-2)]">این کاربر حذف شده یا UUID اشتباه است.</p>
<button onClick={() => navigate('/admin/users')} className="cp-btn-secondary mt-2">
بازگشت به لیست کاربران
</button>
@@ -340,16 +340,16 @@ export default function UserDetailPage() {
<div className="animate-slide-up space-y-5">
{/* ── Breadcrumb / Back ─────────────────────────────────── */}
<div className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
<div className="flex items-center gap-2 text-sm text-[var(--text-2)]">
<button
onClick={() => navigate('/admin/users')}
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"
>
<ArrowRightIcon className="w-4 h-4" />
کاربران
</button>
<span>/</span>
<span className="text-slate-700 dark:text-slate-300 font-medium">پروفایل کاربر</span>
<span className="text-[var(--text)] font-medium">پروفایل کاربر</span>
</div>
{/* ── Profile Card ──────────────────────────────────────── */}
@@ -363,8 +363,8 @@ export default function UserDetailPage() {
<BigAvatar name={user.name} id={user.id} />
<div className="flex-1 min-w-0 sm:mb-1">
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-50 leading-tight">
{user.name || <span className="text-slate-400 font-normal">نام ثبت نشده</span>}
<h1 className="text-xl font-bold text-[var(--text)] dark:text-slate-50 leading-tight">
{user.name || <span className="text-[var(--text-3)] font-normal">نام ثبت نشده</span>}
</h1>
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full ${roleMeta.cls}`}>
@@ -372,15 +372,15 @@ export default function UserDetailPage() {
{roleMeta.label}
</span>
{user.is_active ? (
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-green-100 dark:bg-green-400/10 text-green-700 dark:text-green-300">
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--success-bg)] dark:bg-green-400/10 text-[var(--success)] dark:text-green-300">
<span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />فعال
</span>
) : (
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-slate-100 dark:bg-slate-400/10 text-slate-500 dark:text-slate-400">
<span className="inline-flex items-center gap-1 text-xs font-medium px-2.5 py-0.5 rounded-full bg-[var(--surface-2)] dark:bg-slate-400/10 text-[var(--text-2)]">
<span className="w-1.5 h-1.5 rounded-full bg-slate-400" />غیرفعال
</span>
)}
<span className="text-xs text-slate-400 dark:text-slate-500">#{user.id}</span>
<span className="text-xs text-[var(--text-3)]">#{user.id}</span>
</div>
</div>
@@ -404,29 +404,29 @@ export default function UserDetailPage() {
<EllipsisVerticalIcon className="w-4 h-4" />
</button>
{menuOpen && (
<div className="absolute left-0 top-12 z-30 w-52 cp-card shadow-2xl border border-slate-200 dark:border-gray-700 py-1.5 animate-scale-in" data-upmenu>
<div className="absolute left-0 top-12 z-30 w-52 cp-card shadow-2xl border border-[var(--border)] py-1.5 animate-scale-in" data-upmenu>
<button
onClick={() => { toggleStatusMut.mutate(); setMenuOpen(false); }}
disabled={toggleStatusMut.isPending}
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-gray-800 transition-colors disabled:opacity-50"
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--text)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] transition-colors disabled:opacity-50"
>
{user.is_active
? <XCircleIcon className="w-4 h-4 text-orange-400 shrink-0" />
: <CheckCircleIcon className="w-4 h-4 text-green-500 shrink-0" />
: <CheckCircleIcon className="w-4 h-4 text-[var(--success)] shrink-0" />
}
{user.is_active ? 'غیرفعال کردن حساب' : 'فعال‌سازی حساب'}
</button>
<button
onClick={() => { setRoleOpen(true); setMenuOpen(false); }}
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-gray-800 transition-colors"
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-[var(--text)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface)] transition-colors"
>
<ShieldCheckIcon className="w-4 h-4 text-violet-400 shrink-0" />
تغییر نقش کاربر
</button>
<div className="border-t border-slate-100 dark:border-gray-700 my-1" />
<div className="border-t border-[var(--border)] my-1" />
<button
onClick={() => { 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 transition-colors"
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 transition-colors"
>
<TrashIcon className="w-4 h-4 shrink-0" />
حذف این کاربر
@@ -443,7 +443,7 @@ export default function UserDetailPage() {
{/* Left: Contact info */}
<div className="lg:col-span-2 cp-card p-6 space-y-4">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">اطلاعات تماس و حساب</h2>
<h2 className="text-sm font-semibold text-[var(--text)]">اطلاعات تماس و حساب</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<InfoCard icon={UserIcon} label="نام کامل" value={user.name} />
<InfoCard icon={PhoneIcon} label="شماره موبایل" value={user.mobile_number} mono copyable />
@@ -459,10 +459,10 @@ export default function UserDetailPage() {
{/* Roles card */}
<div className="cp-card p-5 space-y-3">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">نقشهای سیستمی</h2>
<h2 className="text-sm font-semibold text-[var(--text)]">نقشهای سیستمی</h2>
<div className="flex flex-wrap gap-2">
{user.roles.map((r) => (
<span key={r} className="text-xs font-mono px-2.5 py-1 rounded-lg bg-slate-100 dark:bg-gray-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-gray-700">
<span key={r} className="text-xs font-mono px-2.5 py-1 rounded-lg bg-[var(--surface-2)] text-[var(--text-2)] border border-[var(--border)]">
{r}
</span>
))}
@@ -477,14 +477,14 @@ export default function UserDetailPage() {
{/* UUID card */}
<div className="cp-card p-5 space-y-2">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">شناسه یکتا (UUID)</h2>
<h2 className="text-sm font-semibold text-[var(--text)]">شناسه یکتا (UUID)</h2>
<div className="flex items-center gap-2">
<p className="text-[11px] font-mono text-slate-500 dark:text-slate-400 break-all flex-1" dir="ltr">
<p className="text-[11px] font-mono text-[var(--text-2)] break-all flex-1" dir="ltr">
{user.uuid}
</p>
<button
onClick={() => { navigator.clipboard.writeText(user.uuid); toast.success('UUID کپی شد'); }}
className="w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 hover:bg-slate-100 dark:hover:bg-gray-700 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)] dark:hover:text-[var(--text-3)] hover:bg-[var(--surface-2)] dark:hover:bg-[var(--surface-2)] transition-colors shrink-0"
>
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
</button>
@@ -494,13 +494,13 @@ export default function UserDetailPage() {
{/* Status card */}
<div className="cp-card p-5">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">وضعیت حساب</h2>
<h2 className="text-sm font-semibold text-[var(--text)]">وضعیت حساب</h2>
{user.is_active
? <span className="text-xs font-medium text-green-600 dark:text-green-400">فعال</span>
: <span className="text-xs font-medium text-slate-500">غیرفعال</span>
? <span className="text-xs font-medium text-[var(--success)] dark:text-green-400">فعال</span>
: <span className="text-xs font-medium text-[var(--text-2)]">غیرفعال</span>
}
</div>
<div className={`w-full h-2 rounded-full ${user.is_active ? 'bg-green-200 dark:bg-green-400/20' : 'bg-slate-200 dark:bg-gray-700'}`}>
<div className={`w-full h-2 rounded-full ${user.is_active ? 'bg-green-200 dark:bg-green-400/20' : 'bg-[var(--surface-3)] dark:bg-[var(--surface-2)]'}`}>
<div className={`h-full rounded-full transition-all ${user.is_active ? 'w-full bg-green-500' : 'w-0'}`} />
</div>
<button
@@ -509,7 +509,7 @@ export default function UserDetailPage() {
className={`w-full mt-4 text-sm font-medium py-2 rounded-xl transition-colors disabled:opacity-50 ${
user.is_active
? 'bg-orange-50 dark:bg-orange-400/10 text-orange-600 dark:text-orange-400 hover:bg-orange-100 dark:hover:bg-orange-400/20'
: 'bg-green-50 dark:bg-green-400/10 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-400/20'
: 'bg-[var(--success-bg)] dark:bg-green-400/10 text-[var(--success)] dark:text-green-400 hover:bg-[var(--success-bg)] dark:hover:bg-green-400/20'
}`}
>
{toggleStatusMut.isPending
@@ -523,14 +523,14 @@ export default function UserDetailPage() {
{/* ── User Profile (read-only) ──────────────────────────── */}
<div className="cp-card p-6 space-y-4">
<h2 className="text-sm font-semibold text-slate-700 dark:text-slate-300">پروفایل کاربر</h2>
<h2 className="text-sm font-semibold text-[var(--text)]">پروفایل کاربر</h2>
{profileQ.isLoading ? (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{Array.from({ length: 6 }).map((_, i) => <div key={i} className="h-16 rounded-xl skeleton" />)}
</div>
) : !profile ? (
<p className="text-sm text-slate-500 dark:text-slate-400 py-4">
<p className="text-sm text-[var(--text-2)] py-4">
این کاربر هنوز پروفایلی تکمیل نکرده است.
</p>
) : (
@@ -552,8 +552,8 @@ export default function UserDetailPage() {
</div>
{/* Medical history */}
<div className="pt-2 border-t border-slate-100 dark:border-gray-700 space-y-3">
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300">سوابق پزشکی</h3>
<div className="pt-2 border-t border-[var(--border)] space-y-3">
<h3 className="text-sm font-semibold text-[var(--text)]">سوابق پزشکی</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{MEDICAL_SECTIONS.map(({ key, label, format, filter }) => {
const raw = Array.isArray(profile.other?.[key]) ? (profile.other![key] as any[]) : [];
@@ -561,14 +561,14 @@ export default function UserDetailPage() {
.map(format)
.filter((s) => s && s.trim());
return (
<div key={key} className="p-4 rounded-xl bg-slate-50 dark:bg-gray-800/60">
<p className="text-[11px] font-medium text-slate-400 dark:text-slate-500 uppercase tracking-wide mb-1.5">{label}</p>
<div key={key} className="p-4 rounded-xl bg-[var(--surface-2)] dark:bg-[var(--surface)]/60">
<p className="text-[11px] font-medium text-[var(--text-3)] uppercase tracking-wide mb-1.5">{label}</p>
{items.length === 0 ? (
<p className="text-sm text-slate-400 dark:text-slate-500">موردی ثبت نشده</p>
<p className="text-sm text-[var(--text-3)]">موردی ثبت نشده</p>
) : (
<ul className="flex flex-wrap gap-1.5">
{items.map((text, i) => (
<li key={i} className="text-xs px-2 py-0.5 rounded-lg bg-white dark:bg-gray-700 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-gray-600">
<li key={i} className="text-xs px-2 py-0.5 rounded-lg bg-[var(--surface)] text-[var(--text-2)] border border-[var(--border)] dark:border-gray-600">
{text}
</li>
))}
@@ -606,19 +606,19 @@ export default function UserDetailPage() {
<form id="edit-user-form" onSubmit={handleSubmit((v) => updateMut.mutate(v))} className="space-y-4">
{/* Avatar preview */}
<div className="flex items-center gap-4 pb-4 border-b border-slate-100 dark:border-gray-700">
<div className="flex items-center gap-4 pb-4 border-b border-[var(--border)]">
<BigAvatar name={user.name} id={user.id} />
<div>
<p className="text-sm font-semibold text-slate-800 dark:text-slate-200">
<p className="text-sm font-semibold text-[var(--text)]">
{user.name || 'نام ثبت نشده'}
</p>
<p className="text-xs text-slate-500 dark:text-slate-400 font-mono" dir="ltr">{user.mobile_number}</p>
<p className="text-xs text-[var(--text-2)] font-mono" dir="ltr">{user.mobile_number}</p>
</div>
</div>
{/* Name */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
نام و نام خانوادگی
</label>
<input
@@ -627,12 +627,12 @@ export default function UserDetailPage() {
className={`cp-input ${errors.name ? 'border-red-400 dark:border-red-500' : ''}`}
{...register('name')}
/>
{errors.name && <p className="text-xs text-red-500 dark:text-red-400 mt-1">{errors.name.message}</p>}
{errors.name && <p className="text-xs text-[var(--danger)] dark:text-red-400 mt-1">{errors.name.message}</p>}
</div>
{/* Email */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
آدرس ایمیل
</label>
<input
@@ -642,17 +642,17 @@ export default function UserDetailPage() {
className={`cp-input text-left ${errors.email ? 'border-red-400 dark:border-red-500' : ''}`}
{...register('email')}
/>
{errors.email && <p className="text-xs text-red-500 dark:text-red-400 mt-1">{errors.email.message}</p>}
{errors.email && <p className="text-xs text-[var(--danger)] dark:text-red-400 mt-1">{errors.email.message}</p>}
</div>
{/* Password */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
رمز عبور جدید
<span className="text-xs font-normal text-slate-400 dark:text-slate-500 mr-1">(خالی = بدون تغییر)</span>
<span className="text-xs font-normal text-[var(--text-3)] mr-1">(خالی = بدون تغییر)</span>
</label>
<div className="relative">
<KeyIcon className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
<KeyIcon className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-3)] pointer-events-none" />
<input
type="password"
placeholder="حداقل ۶ کاراکتر"
@@ -661,21 +661,21 @@ export default function UserDetailPage() {
{...register('password')}
/>
</div>
{errors.password && <p className="text-xs text-red-500 dark:text-red-400 mt-1">{errors.password.message}</p>}
{errors.password && <p className="text-xs text-[var(--danger)] dark:text-red-400 mt-1">{errors.password.message}</p>}
</div>
{/* Mobile (read-only) */}
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
<label className="block text-sm font-medium text-[var(--text)] mb-1.5">
شماره موبایل
<span className="text-xs font-normal text-slate-400 dark:text-slate-500 mr-1">(قابل تغییر نیست)</span>
<span className="text-xs font-normal text-[var(--text-3)] mr-1">(قابل تغییر نیست)</span>
</label>
<input
type="text"
value={user.mobile_number}
readOnly
dir="ltr"
className="cp-input text-left bg-slate-50 dark:bg-gray-800/60 text-slate-400 cursor-not-allowed"
className="cp-input text-left bg-[var(--surface-2)] dark:bg-[var(--surface)]/60 text-[var(--text-3)] cursor-not-allowed"
/>
</div>
</form>
+24 -24
View File
@@ -5,71 +5,71 @@ import React from 'react';
type IconProps = { size?: number; color?: string };
export function SparkleIcon({ size = 15, color = '#FDD835' }: IconProps) {
export function SparkleIcon({ size = 15, color = 'currentColor' }: IconProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 15 15" fill="none">
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 15 15" fill="none" style={{ color }}>
<path
d="M13.4059 6.94375L11.5309 6.00625L9.85586 5.16875L9.83711 5.15L8.99961 3.475L8.06211 1.6C7.84961 1.175 7.15586 1.175 6.94336 1.6L6.00586 3.475L5.16836 5.15L5.14961 5.16875L3.47461 6.00625L1.59961 6.94375C1.38711 7.05 1.25586 7.26875 1.25586 7.5C1.25586 7.73125 1.38711 7.95 1.59961 8.05625L3.47461 8.99375L5.14961 9.83125L5.16836 9.85L6.00586 11.525L6.94336 13.4C7.04961 13.6125 7.26836 13.7437 7.49961 13.7437C7.73086 13.7437 7.94961 13.6125 8.05586 13.4L8.99336 11.525L9.83086 9.85L9.84961 9.83125L11.5246 8.99375L13.3996 8.05625C13.6121 7.95 13.7434 7.73125 13.7434 7.5C13.7434 7.26875 13.6121 7.05 13.3996 6.94375H13.4059ZM12.1871 0.9375L11.5996 2.225L10.3121 2.8125L11.5996 3.4L12.1871 4.6875L12.7746 3.4L14.0621 2.8125L12.7746 2.225L12.1871 0.9375Z"
fill={color}
fill="currentColor"
/>
</svg>
);
}
export function PlanCardPerson({ size = 20, color = '#2F2F2F' }: IconProps) {
export function PlanCardPerson({ size = 20, color = 'currentColor' }: IconProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 18 18" fill="none">
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 18 18" fill="none" style={{ color }}>
<path
d="M9 9C11.0711 9 12.75 7.32107 12.75 5.25C12.75 3.17893 11.0711 1.5 9 1.5C6.92893 1.5 5.25 3.17893 5.25 5.25C5.25 7.32107 6.92893 9 9 9Z"
stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"
/>
<path
d="M15.4416 16.5C15.4416 13.5975 12.5541 11.25 8.99914 11.25C5.44414 11.25 2.55664 13.5975 2.55664 16.5"
stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"
/>
</svg>
);
}
export function CloseCircleFilled({ size = 20, color = '#D7D7D7' }: IconProps) {
export function CloseCircleFilled({ size = 20, color = 'currentColor' }: IconProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none">
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none" style={{ color }}>
<path
d="M9.99902 2.16699C14.3144 2.16699 17.8328 5.68462 17.833 10C17.833 14.3155 14.3145 17.834 9.99902 17.834C5.68365 17.8338 2.16602 14.3154 2.16602 10C2.16619 5.68473 5.68376 2.16717 9.99902 2.16699ZM13.1533 6.84668C12.7164 6.40977 11.9994 6.40982 11.5625 6.84668L9.99902 8.40918L8.43652 6.84668C7.99959 6.40975 7.28263 6.40975 6.8457 6.84668C6.40877 7.28361 6.40877 8.00057 6.8457 8.4375L8.4082 10L6.8457 11.5635C6.40884 12.0004 6.4088 12.7174 6.8457 13.1543C7.06929 13.3778 7.35703 13.4833 7.64062 13.4834C7.92438 13.4834 8.21282 13.378 8.43652 13.1543L9.99902 11.5908L11.5625 13.1543C11.7861 13.3778 12.0738 13.4833 12.3574 13.4834C12.6412 13.4834 12.9296 13.378 13.1533 13.1543C13.5902 12.7174 13.59 12.0004 13.1533 11.5635L11.5898 10L13.1533 8.4375C13.5901 8.00062 13.5901 7.2836 13.1533 6.84668Z"
fill={color} stroke={color}
fill="currentColor" stroke="currentColor"
/>
</svg>
);
}
export function GiftIcon({ size = 24, color = '#F17732' }: IconProps) {
export function GiftIcon({ size = 24, color = 'currentColor' }: IconProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none">
<path d="M20 12V18C20 20.21 18.21 22 16 22H8C5.79 22 4 20.21 4 18V12C4 11.45 4.45 11 5 11H6.97C7.52 11 7.97 11.45 7.97 12V15.14C7.97 15.88 8.38 16.56 9.03 16.91C9.32 17.07 9.64 17.15 9.97 17.15C10.35 17.15 10.73 17.04 11.06 16.82L12.01 16.2L12.89 16.79C13.5 17.2 14.28 17.25 14.93 16.9C15.59 16.55 16 15.88 16 15.13V12C16 11.45 16.45 11 17 11H19C19.55 11 20 11.45 20 12Z" fill={color} />
<path d="M21.5 7V8C21.5 9.1 20.97 10 19.5 10H4.5C2.97 10 2.5 9.1 2.5 8V7C2.5 5.9 2.97 5 4.5 5H19.5C20.97 5 21.5 5.9 21.5 7Z" fill={color} />
<path d="M11.6388 5.00043H6.11881C5.77881 4.63043 5.78881 4.06043 6.14881 3.70043L7.56881 2.28043C7.93881 1.91043 8.54881 1.91043 8.91881 2.28043L11.6388 5.00043Z" fill={color} />
<path d="M17.8716 5.00043H12.3516L15.0716 2.28043C15.4416 1.91043 16.0516 1.91043 16.4216 2.28043L17.8416 3.70043C18.2016 4.06043 18.2116 4.63043 17.8716 5.00043Z" fill={color} />
<path d="M13.9714 11C14.5214 11 14.9714 11.45 14.9714 12V15.13C14.9714 15.93 14.0814 16.41 13.4214 15.96L12.5214 15.36C12.1914 15.14 11.7614 15.14 11.4214 15.36L10.4814 15.98C9.82141 16.42 8.94141 15.94 8.94141 15.15V12C8.94141 11.45 9.39141 11 9.94141 11H13.9714Z" fill={color} />
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" style={{ color }}>
<path d="M20 12V18C20 20.21 18.21 22 16 22H8C5.79 22 4 20.21 4 18V12C4 11.45 4.45 11 5 11H6.97C7.52 11 7.97 11.45 7.97 12V15.14C7.97 15.88 8.38 16.56 9.03 16.91C9.32 17.07 9.64 17.15 9.97 17.15C10.35 17.15 10.73 17.04 11.06 16.82L12.01 16.2L12.89 16.79C13.5 17.2 14.28 17.25 14.93 16.9C15.59 16.55 16 15.88 16 15.13V12C16 11.45 16.45 11 17 11H19C19.55 11 20 11.45 20 12Z" fill="currentColor" />
<path d="M21.5 7V8C21.5 9.1 20.97 10 19.5 10H4.5C2.97 10 2.5 9.1 2.5 8V7C2.5 5.9 2.97 5 4.5 5H19.5C20.97 5 21.5 5.9 21.5 7Z" fill="currentColor" />
<path d="M11.6388 5.00043H6.11881C5.77881 4.63043 5.78881 4.06043 6.14881 3.70043L7.56881 2.28043C7.93881 1.91043 8.54881 1.91043 8.91881 2.28043L11.6388 5.00043Z" fill="currentColor" />
<path d="M17.8716 5.00043H12.3516L15.0716 2.28043C15.4416 1.91043 16.0516 1.91043 16.4216 2.28043L17.8416 3.70043C18.2016 4.06043 18.2116 4.63043 17.8716 5.00043Z" fill="currentColor" />
<path d="M13.9714 11C14.5214 11 14.9714 11.45 14.9714 12V15.13C14.9714 15.93 14.0814 16.41 13.4214 15.96L12.5214 15.36C12.1914 15.14 11.7614 15.14 11.4214 15.36L10.4814 15.98C9.82141 16.42 8.94141 15.94 8.94141 15.15V12C8.94141 11.45 9.39141 11 9.94141 11H13.9714Z" fill="currentColor" />
</svg>
);
}
export function InfoCircleRedFilled({ size = 20, color = '#D32F2F' }: IconProps) {
export function InfoCircleRedFilled({ size = 20, color = 'currentColor' }: IconProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none">
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 20 20" fill="none" style={{ color }}>
<path
d="M9.99902 2.16699C14.3144 2.16699 17.8328 5.68462 17.833 10C17.833 14.3155 14.3145 17.834 9.99902 17.834C5.68365 17.8338 2.16602 14.3154 2.16602 10C2.16619 5.68473 5.68376 2.16717 9.99902 2.16699ZM10.5088 12.1055C10.1857 11.9709 9.81331 11.9709 9.49023 12.1055C9.3364 12.1696 9.197 12.2588 9.07324 12.3701L9.05371 12.3877L9.03613 12.4072C8.92481 12.5309 8.83561 12.6705 8.77148 12.8242C8.70851 12.9754 8.66602 13.1498 8.66602 13.334C8.66606 13.5127 8.70667 13.6812 8.7666 13.8291L8.76562 13.8301C8.83536 14.0113 8.93351 14.1457 9.03613 14.2598L9.05371 14.2793L9.07324 14.2969C9.197 14.4083 9.33639 14.4974 9.49023 14.5615C9.64129 14.6245 9.81496 14.6669 9.99902 14.667C10.1832 14.667 10.3576 14.6245 10.5088 14.5615C10.6625 14.4974 10.8021 14.4082 10.9258 14.2969L10.9453 14.2793L10.9629 14.2598C11.0631 14.1484 11.1572 14.0168 11.2266 13.8418L11.2275 13.8428C11.2905 13.6917 11.333 13.518 11.333 13.334C11.333 13.1498 11.2905 12.9754 11.2275 12.8242C11.1634 12.6704 11.0742 12.5309 10.9629 12.4072L10.9453 12.3877L10.9258 12.3701L10.8301 12.291C10.7315 12.2159 10.6242 12.1536 10.5088 12.1055ZM9.99902 5.54199C9.38136 5.54217 8.87402 6.04929 8.87402 6.66699V10.834C8.8742 11.4515 9.38147 11.9588 9.99902 11.959C10.6167 11.959 11.1238 11.4516 11.124 10.834V6.66699C11.124 6.04918 10.6168 5.54199 9.99902 5.54199Z"
fill={color} stroke={color}
fill="currentColor" stroke="currentColor"
/>
</svg>
);
}
export function SearchHeaderP({ color = '#7E7E7E' }: { color?: string }) {
export function SearchHeaderP({ color = 'currentColor' }: { color?: string }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M22 22L20 20" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" style={{ color }}>
<path d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M22 22L20 20" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
+15 -23
View File
@@ -260,7 +260,7 @@ body {
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
height: 40px; padding: 0 18px; border-radius: var(--r-sm);
font-size: 14px; font-weight: 700; white-space: nowrap; border: none; cursor: pointer;
background: var(--danger); color: #fff; transition: .16s;
background: var(--danger); color: var(--on-primary); transition: .16s;
}
.cp-btn-danger:hover { filter: brightness(.92); }
.cp-btn-danger:disabled { opacity: .5; cursor: not-allowed; }
@@ -408,7 +408,7 @@ body {
.user-meta span { font-size: 11.5px; color: var(--text-3); }
.cp-avatar {
width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;
display: grid; place-items: center; font-weight: 700; color: #fff; font-size: 13px;
display: grid; place-items: center; font-weight: 700; color: var(--on-primary); font-size: 13px;
background: linear-gradient(145deg, var(--primary), var(--primary-700));
overflow: hidden;
}
@@ -595,7 +595,7 @@ body {
.btn.soft:hover { background: var(--primary-soft2); }
.btn.danger { background: var(--danger-bg); color: var(--danger); border-color: transparent; }
.btn.danger:hover { filter: brightness(.97); }
.btn.accent { background: var(--accent); color: #fff; box-shadow: 0 4px 14px rgba(240,104,42,.35); box-shadow: 0 4px 14px color-mix(in oklch, var(--accent) 35%, transparent); }
.btn.accent { background: var(--accent); color: var(--on-primary); box-shadow: 0 4px 14px rgba(240,104,42,.35); box-shadow: 0 4px 14px color-mix(in oklch, var(--accent) 35%, transparent); }
.btn.accent:hover { background: var(--accent-600); }
.btn.outline { background: transparent; border-color: var(--primary); color: var(--primary); }
.btn.outline:hover { background: var(--primary-soft); }
@@ -668,7 +668,7 @@ table.t tbody tr:last-child td { border-bottom: none; }
transition: transform .18s, background .18s;
}
.switch input:checked ~ .switch-track { background: var(--primary); border-color: var(--primary); }
.switch input:checked ~ .switch-track .switch-thumb { background: #fff; transform: translateX(-20px); }
.switch input:checked ~ .switch-track .switch-thumb { background: var(--on-primary); transform: translateX(-20px); }
.switch input:focus-visible ~ .switch-track { box-shadow: 0 0 0 4px var(--ring); }
/* ── Pagination ──────────────────────────────────────────────── */
@@ -772,7 +772,7 @@ table.t tbody tr:last-child td { border-bottom: none; }
/* ── Colored avatar (hue-based gradient) ─────────────────────── */
.avatar {
width: 38px; height: 38px; border-radius: 50%; flex-shrink: 0;
display: grid; place-items: center; font-weight: 700; color: #fff; font-size: 14px;
display: grid; place-items: center; font-weight: 700; color: var(--on-primary); font-size: 14px;
overflow: hidden;
}
.avatar.sm { width: 32px; height: 32px; font-size: 12px; }
@@ -924,29 +924,23 @@ html, body { max-width: 100%; overflow-x: hidden; }
display: inline-block; width: 100px; text-align: center;
padding: 4px 8px; border-radius: 4px; font-size: 14px; font-weight: 500;
}
.inv-badge.in-stock { background: #E8FADD; color: #3c9a4f; }
.inv-badge.low-stock { background: #FFF3DD; color: #FDBA35; }
.inv-badge.out-of-stock { background: #FFE3E2; color: #FF5450; }
.dark .inv-badge.in-stock { background: #324A32; color: #75E22E; }
.dark .inv-badge.low-stock { background: #4E4234; color: #FDBA35; }
.dark .inv-badge.out-of-stock { background: rgba(255,84,80,0.20); color: #FF5450; }
.inv-badge.in-stock { background: var(--success-bg); color: var(--success); }
.inv-badge.low-stock { background: var(--warning-bg); color: var(--warning); }
.inv-badge.out-of-stock { background: var(--danger-bg); color: var(--danger); }
/* Tab row — mirrors inventory/TabsRow + TabItem. */
.inv-tabs {
display: flex; align-items: flex-end; gap: 24px;
border-bottom: 1px solid #EDEDED; margin: 4px 0 18px; width: 100%;
border-bottom: 1px solid var(--border); margin: 4px 0 18px; width: 100%;
overflow-x: auto; scrollbar-width: none;
}
.inv-tabs::-webkit-scrollbar { display: none; }
.dark .inv-tabs { border-color: #35343D; }
.inv-tab {
display: flex; align-items: center; gap: 6px; padding: 0 8px 6px;
font-size: 16px; cursor: pointer; white-space: nowrap;
border-bottom: 2px solid transparent; color: #6B7280; background: none;
border-bottom: 2px solid transparent; color: var(--text-2); background: none;
}
.dark .inv-tab { color: #A1A1A1; }
.inv-tab.active { border-bottom: 3px solid #5559ce; color: #5559ce; }
.dark .inv-tab.active { color: #5559ce; }
.inv-tab.active { border-bottom: 3px solid var(--primary); color: var(--primary); }
/* Inventory responsive table/card switch (md breakpoint = 768px, like tauri). */
.inv-desktop { display: block; }
@@ -956,19 +950,17 @@ html, body { max-width: 100%; overflow-x: hidden; }
.inv-mobile { display: grid; }
}
.inv-table { width: 100%; border-collapse: collapse; }
.inv-table thead tr { background: #e1e1e1; }
.inv-table thead tr { background: var(--surface-2); }
.inv-table th {
color: #616161; font-size: 14px; font-weight: 400;
color: var(--text-2); font-size: 14px; font-weight: 400;
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;
border-bottom: 1px solid var(--border);
}
.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); }
.inv-table tbody tr:hover { background: var(--surface-2); }
/* input[type=time] با آیکون سفارشی (گام «ایجاد سرویس»): آیکون native مرورگر حذف
می‌شود تا ساعت دوتایی نشود مثل مبدأ tauri که فقط یک ClockField دارد. */
+83
View File
@@ -0,0 +1,83 @@
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
import { describe, expect, it } from 'vitest';
/**
* گاردِ تم: رنگ‌ها باید از توکنهای `styles.css` بیایند تا دارکمود همهجا کار کند.
* hex ثابت و کلاسهای پالت Tailwind با تغییر تم عوض نمیشوند.
*/
const ROOT = join(__dirname, '..');
/** فاز ۳ — هنوز جفت slate/gray + dark: دارند؛ کار می‌کنند ولی از design-system منحرف‌اند. */
const DEFERRED = [
'pages/DoctorDetailPage.tsx',
'pages/UserDetailPage.tsx',
'components/schedule/ScheduleSection.tsx',
];
/** رنگ‌های عمداً ثابت: دادهٔ کاربر یا پوششی که در هر دو تم یکسان است. */
const INTENTIONAL = [
'pages/TagsSettingsPage.tsx', // TAG_COLORS — پالت انتخابی کاربر برای برچسب
'components/ImageCropModal.tsx', // bg-black/50 — اسکریم مودال، مثل .overlay
'stores/uiStore.ts', // BrandFixed — هیوی نارنجی عمداً hex ثابت است
];
const HEX = /#[0-9a-fA-F]{3,8}\b/;
const PALETTE_UTIL =
/(?:^|[\s"'`{])(?:dark:|hover:|focus:|active:|group-hover:|md:|lg:|sm:)*(?:bg|text|border|divide|ring|fill|stroke)-(?:white|black|gray|slate|zinc|neutral|stone|red|green|emerald|blue|sky|amber|yellow|orange|purple|indigo|violet|pink|rose|teal|cyan)(?:-\d{2,3})?\b/;
function sourceFiles(): string[] {
const out: string[] = [];
(function walk(dir: string) {
for (const entry of readdirSync(dir)) {
const p = join(dir, entry);
if (statSync(p).isDirectory()) { walk(p); continue; }
if (!/\.tsx?$/.test(entry) || /\.test\.tsx?$/.test(entry)) continue;
const rel = relative(ROOT, p);
if (rel.startsWith('test/')) continue;
out.push(rel);
}
})(ROOT);
return out;
}
/** کامنت‌ها و fallbackهای `var(--x, #hex)` رنگِ رندرشده نیستند. */
function strip(src: string): string {
return src
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/(^|[^:])\/\/.*$/gm, '$1')
.replace(/var\(\s*--[a-z0-9-]+\s*,[^)]*\)/gi, 'var(--token)');
}
describe('theme tokens', () => {
const files = sourceFiles();
it('سورس ادمین را واقعاً پیدا می‌کند', () => {
expect(files.length).toBeGreaterThan(100);
});
it('هیچ کلاس dark: مرده‌ای کنار inline style رنگی نمانده است', () => {
const offenders = files.filter((f) => {
const src = readFileSync(join(ROOT, f), 'utf8');
return src.split('\n').some((line) =>
/style=\{\{[^}]*(?:color|background)/.test(line)
&& /className=[^>]*\bdark:(?:text|bg|border)-/.test(line));
});
expect(offenders).toEqual([]);
});
it('فایل‌های پاک‌شده hex ثابت ندارند', () => {
const offenders = files
.filter((f) => ![...DEFERRED, ...INTENTIONAL].includes(f))
.filter((f) => HEX.test(strip(readFileSync(join(ROOT, f), 'utf8'))));
expect(offenders).toEqual([]);
});
it('فایل‌های پاک‌شده کلاس پالت Tailwind ندارند', () => {
const offenders = files
.filter((f) => ![...DEFERRED, ...INTENTIONAL].includes(f))
.filter((f) => PALETTE_UTIL.test(strip(readFileSync(join(ROOT, f), 'utf8'))));
expect(offenders).toEqual([]);
});
});
+71 -43
View File
@@ -7,44 +7,55 @@
### رنگ‌بندی (Color Palette)
> **منبع حقیقت `assets/admin/styles.css` است.** توکن‌ها در `:root` تعریف و در
> `[data-theme="dark"]` بازتعریف می‌شوند؛ نسخهٔ oklch/color-mix زیر `@supports` می‌آید و
> مرورگر قدیمی روی همان مقادیر sRGB می‌ماند. هیچ توکنی نباید فقط داخل `@supports` تعریف شود.
```css
/* Primary — Purple (Panelix style) */
--color-primary-50: #f5f3ff;
--color-primary-100: #ede9fe;
--color-primary-200: #ddd6fe;
--color-primary-300: #c4b5fd;
--color-primary-400: #a78bfa;
--color-primary-500: #8b5cf6; /* main */
--color-primary-600: #7c3aed;
--color-primary-700: #6d28d9;
--color-primary-800: #5b21b6;
--color-primary-900: #4c1d95;
/* Neutrals */
--color-gray-50: #f9fafb;
--color-gray-100: #f3f4f6;
--color-gray-200: #e5e7eb;
--color-gray-300: #d1d5db;
--color-gray-400: #9ca3af;
--color-gray-500: #6b7280;
--color-gray-600: #4b5563;
--color-gray-700: #374151;
--color-gray-800: #1f2937;
--color-gray-900: #111827;
/* Status Colors */
--color-success: #10b981;
--color-warning: #f59e0b;
--color-danger: #ef4444;
--color-info: #3b82f6;
/* Background */
--color-bg-body: #f1f5f9; /* light gray page bg */
--color-bg-card: #ffffff;
--color-bg-sidebar: #0f172a; /* dark navy sidebar */
--color-bg-sidebar-active: rgba(139, 92, 246, 0.15);
/* Brand — indigo (نه بنفشِ این سند در نسخه‌های قدیمی) */
--primary --primary-600 --primary-700 --primary-soft --primary-soft2 --on-primary
/* Accent — نارنجی هم‌خانواده با clinic-pro-tauri */
--accent --accent-600 --accent-bg
/* سطوح و متن */
--bg --bg-2 --surface --surface-2 --surface-3 --border --border-2
--text --text-2 --text-3
/* وضعیت */
--success/-bg --warning/-bg --danger/-bg --info/-bg --violet/-bg
/* کارت‌های آمار داشبورد */
--stat-{amber,violet,green,pink}-{bg,fg}
```
**قاعدهٔ الزامی:** در `assets/admin` رنگ ثابت ممنوع است — نه hex، نه کلاس پالت Tailwind
(`bg-white`, `text-gray-800`, `border-slate-200`, …). به‌جایش:
| به‌جای | بنویس |
|---|---|
| `style={{ color: '#525252' }}` | `style={{ color: 'var(--text)' }}` |
| `className="text-[#525252] dark:text-[#D7D8ED]"` | `className="text-[var(--text)]"` |
| `className="bg-white border border-gray-100"` | `className="bg-[var(--surface)] border border-[var(--border)]"` |
| `stroke="#616161"` روی SVG | `style={{ stroke: 'var(--text-2)' }}` یا `stroke="currentColor"` |
نگاشت hexهای قدیمی به توکن (از آدیت ۱۴۰۵/۰۵):
| hex | توکن |
|---|---|
| `#525252` `#3b3b3b` `#2f2f2f` `#111827` `#374151` · دارک: `#d7d8ed` | `--text` |
| `#616161` `#6b7280` `#7e7e7e` `#858d9d` · دارک: `#a1a1a1` | `--text-2` |
| `#9ca3af` `#94a3b8` `#9b9b9b` | `--text-3` |
| `#5559ce` | `--primary` · `#494cb3``--primary-600` · `#3b3f9f``--primary-700` |
| `#f17732` `#f0682a` `#f97316` | `--accent` |
| `#ffffff` (سطح کارت) · دارک: `#222433` | `--surface` |
| `#efefef` `#ededed` `#e7e7e7` · دارک: `#35343d` `#343645` | `--border` |
| `#e0e0e0` `#e1e1e1` `#dbdbdb` `#d7d7d7` · دارک: `#404040` | `--border-2` |
| `#ef4444` `#d32f2f` `#e53935` | `--danger` (پس‌زمینه: `--danger-bg`) |
| `#2e7d32` `#3c9a4f` `#22c55e` | `--success` (پس‌زمینه: `--success-bg`) |
| `#f59e0b` `#fdd835` `#ffc051` | `--warning` (پس‌زمینه: `--warning-bg`) |
| `#f4f5fd` `#f7f8ff` `#eef0ff` | `--primary-soft` · `#e8ebff` `#c7cef4``--primary-soft2` |
استثناهای پذیرفته‌شده (در تست گارد ثبت شده‌اند): پالت انتخابی برچسب‌ها
(`TagsSettingsPage`)، اسکریم مودال (`bg-black/50`، مثل `.overlay`)، و `BrandFixed`
هیوی نارنجی در `stores/uiStore.ts`.
### تایپوگرافی
```
@@ -442,17 +453,34 @@ Row 3: [Bar Chart — آمار ماهانه نمایندگان (60%)] | [لیس
---
## 16. Dark Mode (اختیاری — فاز دوم)
## 16. Dark Mode (پیاده‌شده)
دارک‌مود کامل شیپ شده است. `stores/uiStore.ts``applyTheme()` روی `<html>` هم‌زمان
کلاس `dark` (برای واریانت Tailwind) و `data-theme="dark"` (برای توکن‌های CSS) را ست
می‌کند و مقدارش در `localStorage['clinicpro-ui']` می‌ماند. `applyBrand()` هم
`--brand-h/--brand-c` و در صورت وجود `BrandFixed` مقادیر `--primary*` را inline می‌گذارد.
```css
/* با Tailwind dark: variant */
.dark {
--color-bg-body: #0f172a;
--color-bg-card: #1e293b;
--color-bg-sidebar: #0a0f1e;
}
/* styles.css */
@custom-variant dark (&:where(.dark, .dark *));
:root { --surface: #ffffff; --text: #3b3b3b; /* … */ }
[data-theme="dark"] { --surface: #222433; --text: #D7D8ED; /* … */ }
```
Toggle در topbar ← ذخیره در `localStorage`
قواعد:
- رنگ از توکن بیاید؛ آن‌وقت دارک‌مود **خودکار** درست است و `dark:` لازم نیست.
- `dark:` فقط وقتی که واقعاً توکنِ متفاوتی در دو تم لازم است — نه برای جبران hex ثابت.
- **inline style همیشه بر کلاس غالب است.** `style={{ color: '#525252' }}` کنار
`className="dark:text-[#D7D8ED]"` یعنی کلاس هیچ‌وقت اعمال نمی‌شود؛ این الگو در
تست گارد `assets/admin/test/theme-tokens.test.ts` fail می‌دهد.
- `var(--token)` داخل `style` کار می‌کند؛ برای SVG یا `stroke="currentColor"` بگذار و
رنگ را از والد بگیر، یا `style={{ stroke: 'var(--…)' }}`.
- الفای هگزی (`${color}30`) روی توکن بی‌معنی است — `color-mix(in srgb, var(--x) 19%, transparent)`.
فاز باقی‌مانده: `pages/DoctorDetailPage.tsx`، `pages/UserDetailPage.tsx` و
`components/schedule/ScheduleSection.tsx` هنوز جفت `slate/gray` + `dark:` دارند
(کار می‌کنند ولی با brand hue هماهنگ نمی‌شوند) — در تست گارد به‌عنوان `DEFERRED` علامت خورده‌اند.
---