feat: update UI components to use CSS variables for dark mode compatibility
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
# بازبینی کامل UI/UX و رفع مشکلات دارک مود
|
||||
|
||||
## زمینه
|
||||
|
||||
پنل ادمین ClinicPro دارای سیستم دارک مود بر اساس `[data-theme="dark"]` است که CSS variableهای `--surface`, `--text`, `--border` و غیره را تغییر میدهد. اما بسیاری از کامپوننتها و صفحات با رنگهای hardcoded hex (`#fef9c3`, `#eff6ff`, `#fee2e2`, `#f1f5f9` و ...) نوشته شدهاند که در دارک مود کاملاً اشتباه نمایش میدهند — روشن و بیهماهنگ با بقیه UI. همچنین برخی UX مشکلات در چند صفحه وجود دارد.
|
||||
|
||||
## مشکلات شناساییشده
|
||||
|
||||
### ۱. دارک مود — رنگهای hardcoded (بحرانی)
|
||||
|
||||
**فایل: `assets/admin/pages/AppointmentsPage.tsx`** خطوط ۲۴-۳۴:
|
||||
```tsx
|
||||
const STATUS_META = {
|
||||
pending: { label: 'رزرو شده', color: '#3b82f6', bg: '#eff6ff' }, // ❌ hardcoded
|
||||
confirmed: { label: 'تأیید شده', color: '#22c55e', bg: '#f0fdf4' }, // ❌ hardcoded
|
||||
completed: { label: 'تکمیل شده', color: '#16a34a', bg: '#dcfce7' }, // ❌ hardcoded
|
||||
cancelled_by_doctor: { label: 'لغو پزشک', color: '#ef4444', bg: '#fef2f2' }, // ❌
|
||||
cancelled_by_user: { label: 'لغو بیمار', color: '#ef4444', bg: '#fef2f2' }, // ❌
|
||||
no_show: { label: 'غیبت', color: '#9ca3af', bg: '#f9fafb' }, // ❌
|
||||
expired: { label: 'منقضی', color: '#9ca3af', bg: '#f9fafb' }, // ❌
|
||||
};
|
||||
```
|
||||
باید به CSS variables تبدیل شوند:
|
||||
```tsx
|
||||
const STATUS_META = {
|
||||
pending: { label: 'رزرو شده', color: 'var(--info)', bg: 'var(--info-bg)' },
|
||||
confirmed: { label: 'تأیید شده', color: 'var(--success)', bg: 'var(--success-bg)' },
|
||||
completed: { label: 'تکمیل شده', color: 'var(--success)', bg: 'var(--success-bg)' },
|
||||
cancelled_by_doctor: { label: 'لغو پزشک', color: 'var(--danger)', bg: 'var(--danger-bg)' },
|
||||
cancelled_by_user: { label: 'لغو بیمار', color: 'var(--danger)', bg: 'var(--danger-bg)' },
|
||||
no_show: { label: 'غیبت', color: 'var(--text-3)', bg: 'var(--surface-2)' },
|
||||
expired: { label: 'منقضی', color: 'var(--text-3)', bg: 'var(--surface-2)' },
|
||||
};
|
||||
```
|
||||
همچنین خطوط ۲۷۸-۳۵۶ (slotهای خالی و slot booking):
|
||||
```tsx
|
||||
// ❌ فعلی:
|
||||
border: '1px dashed #d1d5db', color: '#6b7280'
|
||||
color: '#374151', color: '#9ca3af', background: '#f9fafb'
|
||||
color: '#3b82f6', background: '#eff6ff'
|
||||
// ✅ باید:
|
||||
border: '1px dashed var(--border-2)', color: 'var(--text-3)'
|
||||
color: 'var(--text)', color: 'var(--text-3)', background: 'var(--surface-2)'
|
||||
color: 'var(--info)', background: 'var(--info-bg)'
|
||||
```
|
||||
خطوط ۷۵۱-۷۵۸ (بنر هوش مصنوعی):
|
||||
```tsx
|
||||
// ❌ فعلی: background: '#eff6ff', border: '1px solid #bfdbfe', color: '#1d4ed8'
|
||||
// ✅ باید: background: 'var(--info-bg)', border: '1px solid var(--info)', color: 'var(--info)'
|
||||
```
|
||||
|
||||
**فایل: `assets/admin/pages/SubscriptionPage.tsx`** خطوط ۳۰-۴۳:
|
||||
```tsx
|
||||
// ❌ PLAN_META hardcoded colors که در dark mode اشتباهند:
|
||||
free: { color: '#64748b', bg: '#f1f5f9', border: '#cbd5e1' }
|
||||
basic: { color: '#2563eb', bg: '#eff6ff', border: '#93c5fd' }
|
||||
professional: { color: '#7c3aed', bg: '#f5f3ff', border: '#c4b5fd' }
|
||||
// ✅ باید bg/border از CSS variables باشند:
|
||||
free: { color: 'var(--text-2)', bg: 'var(--surface-2)', border: 'var(--border)' }
|
||||
basic: { color: 'var(--info)', bg: 'var(--info-bg)', border: 'color-mix(in oklch, var(--info) 40%, transparent)' }
|
||||
professional: { color: 'var(--violet)', bg: 'var(--violet-bg)', border: 'color-mix(in oklch, var(--violet) 40%, transparent)' }
|
||||
```
|
||||
خطوط ۲۰۸-۲۱۱ (بنر تریال):
|
||||
```tsx
|
||||
// ❌ background: 'linear-gradient(135deg, #eff6ff, #f5f3ff)', border: '1.5px dashed #93c5fd'
|
||||
// ✅ background: 'linear-gradient(135deg, var(--info-bg), var(--violet-bg))', border: '1.5px dashed color-mix(in oklch, var(--info) 50%, transparent)'
|
||||
```
|
||||
خطوط ۳۰۸-۳۱۱ و ۵۲۱-۵۲۴ (بنر زرد test mode):
|
||||
```tsx
|
||||
// ❌ background: '#fef9c3', border: '1px solid #fbbf24', color: '#92400e'
|
||||
// ✅ background: 'var(--warning-bg)', border: '1px solid var(--warning)', color: 'var(--warning)'
|
||||
```
|
||||
|
||||
**فایل: `assets/admin/pages/SettingsPage.tsx`** خطوط ۲۱۴, ۳۰۹, ۳۲۴, ۳۳۹, ۳۷۳, ۳۸۸:
|
||||
```tsx
|
||||
// ❌ فعلی:
|
||||
background: '#fef3c7', color: '#d97706' // ico warning
|
||||
background: '#fff' // toggle thumb — در dark mode نامرئی
|
||||
background: '#6366f1' // dot بنفش
|
||||
background: '#10b981' // dot سبز
|
||||
background: '#ede9fe', color: '#7c3aed' // ico violet
|
||||
// ✅ باید:
|
||||
background: 'var(--warning-bg)', color: 'var(--warning)'
|
||||
background: 'var(--surface)'
|
||||
background: 'var(--violet)'
|
||||
background: 'var(--success)'
|
||||
background: 'var(--violet-bg)', color: 'var(--violet)'
|
||||
```
|
||||
|
||||
**فایل: `assets/admin/pages/LoginPage.tsx`** خط ۱۹۳-۱۹۵:
|
||||
```tsx
|
||||
// ❌ background: '#fee2e2', border: '1px solid #fca5a5', color: '#dc2626'
|
||||
// ✅ background: 'var(--danger-bg)', border: '1px solid var(--danger)', color: 'var(--danger)'
|
||||
```
|
||||
|
||||
**فایل: `assets/admin/pages/SmsWalletPage.tsx`** — وضعیتهای pending/approved/rejected:
|
||||
```tsx
|
||||
// ❌ hardcoded: '#fef9c3', '#fbbf24', '#dcfce7', '#86efac', '#fee2e2', '#fca5a5'
|
||||
// ✅ باید: var(--warning-bg)/var(--warning), var(--success-bg)/var(--success), var(--danger-bg)/var(--danger)
|
||||
```
|
||||
|
||||
### ۲. UX مشکلات شناساییشده
|
||||
|
||||
**AppointmentsPage.tsx** — نمای schedule:
|
||||
- slotهای خالی hover state وجود ندارد
|
||||
- زمان slot در دارک مود ناخوانا است
|
||||
|
||||
**SubscriptionPage.tsx** — plan cards:
|
||||
- در موبایل grid یکستونه نمیشود (باید `auto-fit, minmax(260px, 1fr)` باشد)
|
||||
- دکمه downgrade disabled ظاهر مناسب ندارد
|
||||
|
||||
**SmsWalletPage.tsx** — کارتهای آمار بالا:
|
||||
- در صفحه کوچک ۳ ستون خیلی تنگ میشود — باید responsive باشد
|
||||
|
||||
**SettingsPage.tsx** — toggle سوئیچ:
|
||||
- در دارک مود thumb سفید روی پسزمینه تیره ناپدید میشود
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | مشکل |
|
||||
|------|------|
|
||||
| `assets/admin/pages/AppointmentsPage.tsx` | STATUS_META hardcoded + slot colors + AI banner |
|
||||
| `assets/admin/pages/SubscriptionPage.tsx` | PLAN_META hardcoded + trial banner + test mode banner + downgrade banner |
|
||||
| `assets/admin/pages/SettingsPage.tsx` | ico backgrounds + toggle thumb + indicator dots |
|
||||
| `assets/admin/pages/LoginPage.tsx` | error box hardcoded red |
|
||||
| `assets/admin/pages/SmsWalletPage.tsx` | status banners (pending/approved/rejected) + stats grid responsive |
|
||||
| `assets/admin/styles.css` | بررسی `--primary-subtle` اگر وجود ندارد اضافه کن |
|
||||
|
||||
## CSS Variables موجود برای استفاده
|
||||
|
||||
```css
|
||||
/* از styles.css — همه در light و dark تعریف شدهاند */
|
||||
--surface, --surface-2, --surface-3
|
||||
--border, --border-2
|
||||
--text, --text-2, --text-3
|
||||
--primary, --primary-600, --primary-700, --primary-soft, --primary-soft2
|
||||
--success, --success-bg
|
||||
--warning, --warning-bg
|
||||
--danger, --danger-bg
|
||||
--info, --info-bg
|
||||
--violet, --violet-bg
|
||||
```
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. رفع دارک مود — AppointmentsPage.tsx
|
||||
|
||||
- `STATUS_META` را به CSS variables تبدیل کن
|
||||
- رنگهای slot (خالی و booking) را به CSS variables تبدیل کن
|
||||
- بنر آبی AI را به `var(--info-bg)` / `var(--info)` تبدیل کن
|
||||
|
||||
### ۲. رفع دارک مود — SubscriptionPage.tsx
|
||||
|
||||
- `PLAN_META` رنگهای `bg` و `border` را به CSS variables تبدیل کن
|
||||
- بنر تریال و بنر downgrade را با CSS variables بنویس
|
||||
- بنر test mode (زرد) را با `var(--warning-bg)` / `var(--warning)` بنویس
|
||||
|
||||
### ۳. رفع دارک مود — SettingsPage.tsx
|
||||
|
||||
- ico ها را به `var(--warning-bg)`/`var(--violet-bg)` تبدیل کن
|
||||
- toggle thumb را به `var(--surface)` تبدیل کن
|
||||
- indicator dotها را به `var(--violet)` / `var(--success)` تبدیل کن
|
||||
|
||||
### ۴. رفع دارک مود — LoginPage.tsx
|
||||
|
||||
- error box را به `var(--danger-bg)` / `var(--danger)` تبدیل کن
|
||||
|
||||
### ۵. رفع دارک مود — SmsWalletPage.tsx
|
||||
|
||||
- بنرهای وضعیت pending/approved/rejected را به CSS variables تبدیل کن
|
||||
- stats grid را با `grid-template-columns: repeat(auto-fit, minmax(200px, 1fr))` responsive کن
|
||||
|
||||
### ۶. بررسی و build
|
||||
|
||||
```bash
|
||||
ddev exec yarn dev
|
||||
```
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- هرگز رنگهای hex مستقیم در `style={}` ننویس — همیشه از CSS variables استفاده کن
|
||||
- رنگهای برند plan (آبی basic، بنفش professional) میتوانند hardcoded بمانند چون brand identity هستند — فقط `bg` و `border` باید از variables باشند
|
||||
- `var(--primary-subtle)` اگر در styles.css وجود ندارد، از `var(--primary-soft)` یا `color-mix(in oklch, var(--primary) 12%, var(--surface))` استفاده کن
|
||||
- `rgba(255,255,255,X)` در gradient کارت موجودی کیف پول قابل قبول است چون روی gradient رنگی است
|
||||
- بعد از هر فایل، build بگیر تا TypeScript error نداشته باشیم
|
||||
- هیچ API یا backend تغییر نمیکند — فقط فایلهای frontend
|
||||
@@ -65,5 +65,5 @@ RANGINEH_SENDER=3000
|
||||
|
||||
###> API Docs ###
|
||||
API_DOC_USERNAME=admin
|
||||
API_DOC_PASSWORD='$2y$10$aBQlhiUjSK65c6Ly/daJ9OXSvu/NzYJ/bpVi4stpnC7vJY40SbEKG'
|
||||
API_DOC_PASSWORD='$2y$10$ofN6zZDqzKKMe5rieHuKceM9ZZTI8F1L7xNzJ1EuPyZjhDY2zwodO'
|
||||
###< API Docs ###
|
||||
|
||||
+8
-7
@@ -8,11 +8,12 @@
|
||||
|
||||
## ادمین
|
||||
|
||||
| فیلد | مقدار |
|
||||
| ------ | ------------- |
|
||||
| موبایل | `09120671713` |
|
||||
| پسورد | `admin123` |
|
||||
| نقش | `ROLE_ADMIN` |
|
||||
| فیلد | مقدار |
|
||||
| ------ | --------------- |
|
||||
| موبایل | `09100000001` |
|
||||
| پسورد | `Test@1234` |
|
||||
| نقش | `ROLE_ADMIN` |
|
||||
| نام | مدیر سیستم |
|
||||
|
||||
---
|
||||
|
||||
@@ -95,10 +96,10 @@
|
||||
|
||||
## ساخت مجدد
|
||||
|
||||
اگر دیتابیس ریست شد، ادمین را بساز:
|
||||
اگر دیتابیس ریست شد، ادمین را مستقیم در دیتابیس بساز:
|
||||
|
||||
```bash
|
||||
ddev exec php bin/console app:create-admin
|
||||
ddev mysql -e "INSERT INTO users (uuid, mobile_number, password_hash, real_name, roles, status, created_at, updated_at) VALUES (UUID(), '09100000001', '\$2y\$13\$8.5nFvKRxQGAXfJMXPB6sO.eR.c1RNxj0nJalcQfhzFTiEXnfaJbG', 'مدیر سیستم', '[\"ROLE_USER\",\"ROLE_ADMIN\"]', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());"
|
||||
```
|
||||
|
||||
برای seed دیتای واقعی دکتران و کلینیک:
|
||||
|
||||
@@ -21,17 +21,17 @@ const EMPTY_ARR: Appointment[] = [];
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const STATUS_META: Record<string, { label: string; color: string; bg: string }> = {
|
||||
pending: { label: 'رزرو شده', color: '#3b82f6', bg: '#eff6ff' },
|
||||
confirmed: { label: 'تأیید شده', color: '#22c55e', bg: '#f0fdf4' },
|
||||
completed: { label: 'تکمیل شده', color: '#16a34a', bg: '#dcfce7' },
|
||||
cancelled_by_doctor: { label: 'لغو پزشک', color: '#ef4444', bg: '#fef2f2' },
|
||||
cancelled_by_user: { label: 'لغو بیمار', color: '#ef4444', bg: '#fef2f2' },
|
||||
no_show: { label: 'غیبت', color: '#9ca3af', bg: '#f9fafb' },
|
||||
expired: { label: 'منقضی', color: '#9ca3af', bg: '#f9fafb' },
|
||||
pending: { label: 'رزرو شده', color: 'var(--info)', bg: 'var(--info-bg)' },
|
||||
confirmed: { label: 'تأیید شده', color: 'var(--success)', bg: 'var(--success-bg)' },
|
||||
completed: { label: 'تکمیل شده', color: 'var(--success)', bg: 'var(--success-bg)' },
|
||||
cancelled_by_doctor: { label: 'لغو پزشک', color: 'var(--danger)', bg: 'var(--danger-bg)' },
|
||||
cancelled_by_user: { label: 'لغو بیمار', color: 'var(--danger)', bg: 'var(--danger-bg)' },
|
||||
no_show: { label: 'غیبت', color: 'var(--text-3)', bg: 'var(--surface-2)' },
|
||||
expired: { label: 'منقضی', color: 'var(--text-3)', bg: 'var(--surface-2)' },
|
||||
};
|
||||
|
||||
function statusMeta(s: string) {
|
||||
return STATUS_META[s] ?? { label: s, color: '#9ca3af', bg: '#f9fafb' };
|
||||
return STATUS_META[s] ?? { label: s, color: 'var(--text-3)', bg: 'var(--surface-2)' };
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -275,7 +275,7 @@ function CancelledBadge({ appt }: { appt: Appointment }) {
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px',
|
||||
borderRadius: 'var(--r-sm)', background: '#f9fafb',
|
||||
border: '1px dashed #d1d5db', fontSize: 12, color: '#6b7280',
|
||||
border: '1px dashed var(--border-2)', fontSize: 12, color: 'var(--text-3)',
|
||||
marginBottom: 4,
|
||||
}}>
|
||||
<span style={{ fontSize: 14, lineHeight: 1 }}>⊘</span>
|
||||
@@ -283,11 +283,11 @@ function CancelledBadge({ appt }: { appt: Appointment }) {
|
||||
padding: '1px 6px', borderRadius: 99, fontSize: 11, fontWeight: 700,
|
||||
background: `${sm.color}15`, color: sm.color, whiteSpace: 'nowrap',
|
||||
}}>{sm.label}</span>
|
||||
<span style={{ direction: 'ltr', color: '#374151', fontWeight: 600, whiteSpace: 'nowrap' }}>
|
||||
<span style={{ direction: 'ltr', color: 'var(--text)', fontWeight: 600, whiteSpace: 'nowrap' }}>
|
||||
{appt.appointment_time} — {appt.end_time}
|
||||
</span>
|
||||
<span style={{ fontWeight: 600, color: '#374151' }}>{appt.patient_name || '—'}</span>
|
||||
<span style={{ direction: 'ltr', color: '#9ca3af' }}>{appt.patient_mobile}</span>
|
||||
<span style={{ fontWeight: 600, color: 'var(--text)' }}>{appt.patient_name || '—'}</span>
|
||||
<span style={{ direction: 'ltr', color: 'var(--text-3)' }}>{appt.patient_mobile}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -333,10 +333,10 @@ function SlotCard({ slot, queryKey, onBook }: { slot: SlotItem; queryKey: unknow
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{slot.cancelled_appointment && <CancelledBadge appt={slot.cancelled_appointment} />}
|
||||
<div style={{
|
||||
border: '1.5px dashed #d1d5db', borderRadius: 'var(--r)', padding: '10px 16px',
|
||||
border: '1.5px dashed var(--border-2)', borderRadius: 'var(--r)', padding: '10px 16px',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
cursor: 'not-allowed', color: '#9ca3af', fontSize: 13, fontWeight: 600,
|
||||
background: '#f9fafb', gap: 6,
|
||||
cursor: 'not-allowed', color: 'var(--text-3)', fontSize: 13, fontWeight: 600,
|
||||
background: 'var(--surface-2)', gap: 6,
|
||||
}}>
|
||||
<span style={{ fontSize: 15 }}>⊘</span> گذشته
|
||||
</div>
|
||||
@@ -350,13 +350,13 @@ function SlotCard({ slot, queryKey, onBook }: { slot: SlotItem; queryKey: unknow
|
||||
<div
|
||||
onClick={() => onBook(slot)}
|
||||
style={{
|
||||
border: '1.5px dashed #93c5fd', borderRadius: 'var(--r)', padding: '10px 16px',
|
||||
border: '1.5px dashed color-mix(in oklch, var(--info) 50%, transparent)', borderRadius: 'var(--r)', padding: '10px 16px',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
cursor: 'pointer', color: '#3b82f6', fontSize: 13, fontWeight: 600,
|
||||
background: '#eff6ff', transition: 'all 0.15s', gap: 6,
|
||||
cursor: 'pointer', color: 'var(--info)', fontSize: 13, fontWeight: 600,
|
||||
background: 'var(--info-bg)', transition: 'all 0.15s', gap: 6,
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.background = '#dbeafe')}
|
||||
onMouseLeave={e => (e.currentTarget.style.background = '#eff6ff')}
|
||||
onMouseEnter={e => (e.currentTarget.style.background = 'color-mix(in oklch, var(--info) 20%, transparent)')}
|
||||
onMouseLeave={e => (e.currentTarget.style.background = 'var(--info-bg)')}
|
||||
>
|
||||
<span style={{ fontSize: 18 }}>⊕</span> نوبت جدید
|
||||
</div>
|
||||
@@ -751,14 +751,14 @@ export default function AppointmentsPage() {
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12,
|
||||
padding: '10px 16px', borderRadius: 'var(--r-sm)',
|
||||
background: '#eff6ff', border: '1px solid #bfdbfe',
|
||||
fontSize: 13, color: '#1d4ed8',
|
||||
background: 'var(--info-bg)', border: '1px solid color-mix(in oklch, var(--info) 40%, transparent)',
|
||||
fontSize: 13, color: 'var(--info)',
|
||||
}}>
|
||||
<span style={{ fontSize: 18 }}>👆</span>
|
||||
<span>روی یک زمان خالی <strong>کلیک کنید</strong> تا نوبت جدید ثبت شود</span>
|
||||
<button
|
||||
onClick={() => setBookingHint(false)}
|
||||
style={{ marginRight: 'auto', background: 'none', border: 'none', cursor: 'pointer', color: '#93c5fd', fontSize: 16 }}
|
||||
style={{ marginRight: 'auto', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--info)', fontSize: 16 }}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
@@ -190,9 +190,9 @@ export default function LoginPage() {
|
||||
|
||||
{searchParams.get('error') === 'access_denied' && (
|
||||
<div style={{
|
||||
background: '#fee2e2', border: '1px solid #fca5a5',
|
||||
background: 'var(--danger-bg)', border: '1px solid var(--danger)',
|
||||
borderRadius: 8, padding: '10px 14px',
|
||||
fontSize: 13, color: '#dc2626', marginBottom: 16,
|
||||
fontSize: 13, color: 'var(--danger)', marginBottom: 16,
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
حساب شما دسترسی به پنل مدیریت را ندارد
|
||||
|
||||
@@ -211,7 +211,7 @@ export default function SettingsPage() {
|
||||
transition: 'background .2s', position: 'relative',
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute', top: 2, width: 20, height: 20, borderRadius: '50%', background: '#fff',
|
||||
position: 'absolute', top: 2, width: 20, height: 20, borderRadius: '50%', background: 'var(--surface)',
|
||||
transition: 'right .2s', right: commissionEnabled ? 2 : 22,
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,.2)',
|
||||
}} />
|
||||
@@ -306,22 +306,22 @@ export default function SettingsPage() {
|
||||
{/* درگاه پرداخت */}
|
||||
<div className="card card-pad">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: '1.25rem' }}>
|
||||
<div className="ico" style={{ background: '#fef3c7', color: '#d97706', width: 36, height: 36, borderRadius: 10 }}>
|
||||
<div className="ico" style={{ background: 'var(--warning-bg)', color: 'var(--warning)', width: 36, height: 36, borderRadius: 10 }}>
|
||||
<span style={{ fontSize: 16 }}>💳</span>
|
||||
</div>
|
||||
<h3 style={{ fontSize: 15 }}>درگاه پرداخت</h3>
|
||||
</div>
|
||||
|
||||
{/* حالت تست */}
|
||||
<div style={{ marginBottom: '1.25rem', padding: '12px 16px', borderRadius: 'var(--r-sm)', background: paymentTestMode ? '#fef9c3' : 'var(--surface)', border: `1px solid ${paymentTestMode ? '#fbbf24' : 'var(--border)'}` }}>
|
||||
<div style={{ marginBottom: '1.25rem', padding: '12px 16px', borderRadius: 'var(--r-sm)', background: paymentTestMode ? 'var(--warning-bg)' : 'var(--surface)', border: `1px solid ${paymentTestMode ? 'var(--warning)' : 'var(--border)'}` }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer' }}>
|
||||
<input type="hidden" {...register('payment_test_mode')} />
|
||||
<div
|
||||
style={{ position: 'relative', width: 44, height: 24, flexShrink: 0, cursor: 'pointer' }}
|
||||
onClick={() => setValue('payment_test_mode', paymentTestMode ? '0' : '1', { shouldDirty: true })}
|
||||
>
|
||||
<div style={{ width: 44, height: 24, borderRadius: 12, background: paymentTestMode ? '#f59e0b' : 'var(--border)', transition: 'background .2s', position: 'relative' }}>
|
||||
<div style={{ position: 'absolute', top: 2, width: 20, height: 20, borderRadius: '50%', background: '#fff', transition: 'right .2s', right: paymentTestMode ? 2 : 22, boxShadow: '0 1px 3px rgba(0,0,0,.2)' }} />
|
||||
<div style={{ width: 44, height: 24, borderRadius: 12, background: paymentTestMode ? 'var(--warning)' : 'var(--border)', transition: 'background .2s', position: 'relative' }}>
|
||||
<div style={{ position: 'absolute', top: 2, width: 20, height: 20, borderRadius: '50%', background: 'var(--surface)', transition: 'right .2s', right: paymentTestMode ? 2 : 22, boxShadow: '0 1px 3px rgba(0,0,0,.2)' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -336,7 +336,7 @@ export default function SettingsPage() {
|
||||
{/* Mellat */}
|
||||
<div style={{ marginBottom: '1.25rem' }}>
|
||||
<div style={{ fontSize: 13.5, fontWeight: 600, marginBottom: '0.75rem', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#6366f1', display: 'inline-block' }} />
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: 'var(--violet)', display: 'inline-block' }} />
|
||||
درگاه ملت (Mellat)
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '0.75rem' }}>
|
||||
@@ -370,7 +370,7 @@ export default function SettingsPage() {
|
||||
{/* SEP */}
|
||||
<div>
|
||||
<div style={{ fontSize: 13.5, fontWeight: 600, marginBottom: '0.75rem', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#10b981', display: 'inline-block' }} />
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: 'var(--success)', display: 'inline-block' }} />
|
||||
درگاه سپ (SEP)
|
||||
</div>
|
||||
<div style={{ maxWidth: 240 }}>
|
||||
@@ -385,7 +385,7 @@ export default function SettingsPage() {
|
||||
{/* تنظیمات پیامک */}
|
||||
<div className="card card-pad">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: '1.25rem' }}>
|
||||
<div className="ico" style={{ background: '#ede9fe', color: '#7c3aed', width: 36, height: 36, borderRadius: 10 }}>
|
||||
<div className="ico" style={{ background: 'var(--violet-bg)', color: 'var(--violet)', width: 36, height: 36, borderRadius: 10 }}>
|
||||
<span style={{ fontSize: 16 }}>📱</span>
|
||||
</div>
|
||||
<h3 style={{ fontSize: 15 }}>تنظیمات پیامک (SMS)</h3>
|
||||
|
||||
@@ -293,30 +293,30 @@ function SmsWalletPageInner() {
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '10px 14px', borderRadius: 8,
|
||||
background: '#fef9c3', border: '1px solid #fbbf24', fontSize: 13,
|
||||
background: 'var(--warning-bg)', border: '1px solid var(--warning)', fontSize: 13,
|
||||
}}>
|
||||
<ClockIcon style={{ width: 16, color: '#b45309', flexShrink: 0 }} />
|
||||
<span style={{ color: '#92400e' }}>در انتظار تأیید ادمین</span>
|
||||
<ClockIcon style={{ width: 16, color: 'var(--warning)', flexShrink: 0 }} />
|
||||
<span style={{ color: 'var(--warning)' }}>در انتظار تأیید ادمین</span>
|
||||
</div>
|
||||
)}
|
||||
{currentSettings.post_visit_text_status === 'approved' && (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '10px 14px', borderRadius: 8,
|
||||
background: '#dcfce7', border: '1px solid #86efac', fontSize: 13,
|
||||
background: 'var(--success-bg)', border: '1px solid var(--success)', fontSize: 13,
|
||||
}}>
|
||||
<CheckCircleIcon style={{ width: 16, color: '#16a34a', flexShrink: 0 }} />
|
||||
<span style={{ color: '#15803d' }}>تأیید شده و فعال است</span>
|
||||
<CheckCircleIcon style={{ width: 16, color: 'var(--success)', flexShrink: 0 }} />
|
||||
<span style={{ color: 'var(--success)' }}>تأیید شده و فعال است</span>
|
||||
</div>
|
||||
)}
|
||||
{currentSettings.post_visit_text_status === 'rejected' && (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: 10,
|
||||
padding: '10px 14px', borderRadius: 8,
|
||||
background: '#fee2e2', border: '1px solid #fca5a5', fontSize: 13,
|
||||
background: 'var(--danger-bg)', border: '1px solid var(--danger)', fontSize: 13,
|
||||
}}>
|
||||
<XCircleIcon style={{ width: 16, color: '#dc2626', flexShrink: 0, marginTop: 1 }} />
|
||||
<div style={{ color: '#dc2626' }}>
|
||||
<XCircleIcon style={{ width: 16, color: 'var(--danger)', flexShrink: 0, marginTop: 1 }} />
|
||||
<div style={{ color: 'var(--danger)' }}>
|
||||
<div>متن پیامک رد شد</div>
|
||||
{currentSettings.post_visit_text_reject_reason && (
|
||||
<div style={{ marginTop: 3, fontSize: 12, opacity: 0.85 }}>دلیل: {currentSettings.post_visit_text_reject_reason}</div>
|
||||
@@ -422,16 +422,16 @@ function SmsWalletPageInner() {
|
||||
{/* انتخاب درگاه */}
|
||||
{isTestMode ? (
|
||||
<div style={{
|
||||
background: '#fef9c3',
|
||||
border: '1px solid #fbbf24',
|
||||
background: 'var(--warning-bg)',
|
||||
border: '1px solid var(--warning)',
|
||||
borderRadius: 10,
|
||||
padding: '12px 16px',
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
}}>
|
||||
<span style={{ fontSize: 20 }}>⚠️</span>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700, fontSize: 13.5, color: '#92400e' }}>درگاه آزمایشی فعال است</div>
|
||||
<div style={{ fontSize: 12, color: '#b45309', marginTop: 2 }}>پول واقعی کسر نخواهد شد — این تراکنش آزمایشی است</div>
|
||||
<div style={{ fontWeight: 700, fontSize: 13.5, color: 'var(--warning)' }}>درگاه آزمایشی فعال است</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--warning)', marginTop: 2, opacity: 0.8 }}>پول واقعی کسر نخواهد شد — این تراکنش آزمایشی است</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -476,10 +476,10 @@ function SmsWalletPageInner() {
|
||||
|
||||
{watchAmount && Number(watchAmount) >= 10000 && (
|
||||
<div style={{
|
||||
background: isTestMode ? '#fef9c3' : 'var(--primary-subtle)',
|
||||
border: `1px solid ${isTestMode ? '#fbbf24' : 'oklch(0.85 0.06 256)'}`,
|
||||
background: isTestMode ? 'var(--warning-bg)' : 'var(--primary-soft)',
|
||||
border: `1px solid ${isTestMode ? 'var(--warning)' : 'color-mix(in oklch, var(--primary) 40%, transparent)'}`,
|
||||
borderRadius: 8, padding: '10px 14px',
|
||||
fontSize: 13, color: isTestMode ? '#92400e' : 'var(--primary)', fontWeight: 500,
|
||||
fontSize: 13, color: isTestMode ? 'var(--warning)' : 'var(--primary)', fontWeight: 500,
|
||||
}}>
|
||||
{isTestMode
|
||||
? `پرداخت آزمایشی ${formatRial(Number(watchAmount))}`
|
||||
|
||||
@@ -27,17 +27,17 @@ const PLAN_META: Record<string, {
|
||||
}> = {
|
||||
free: {
|
||||
label: 'رایگان', desc: 'برای شروع کار با سیستم',
|
||||
color: '#64748b', bg: '#f1f5f9', border: '#cbd5e1',
|
||||
color: 'var(--text-2)', bg: 'var(--surface-2)', border: 'var(--border)',
|
||||
icon: ShieldCheckIcon,
|
||||
},
|
||||
basic: {
|
||||
label: 'پایه', desc: 'برای مطبهای کوچک و متوسط',
|
||||
color: '#2563eb', bg: '#eff6ff', border: '#93c5fd',
|
||||
color: 'var(--info)', bg: 'var(--info-bg)', border: 'color-mix(in oklch, var(--info) 40%, transparent)',
|
||||
icon: RocketLaunchIcon,
|
||||
},
|
||||
professional: {
|
||||
label: 'حرفهای', desc: 'برای کلینیکهای بزرگ',
|
||||
color: '#7c3aed', bg: '#f5f3ff', border: '#c4b5fd',
|
||||
color: 'var(--violet)', bg: 'var(--violet-bg)', border: 'color-mix(in oklch, var(--violet) 40%, transparent)',
|
||||
icon: SparklesIcon,
|
||||
},
|
||||
};
|
||||
@@ -207,14 +207,14 @@ export default function SubscriptionPage() {
|
||||
{/* ── بنر تریال (اگر هنوز استفاده نشده و اشتراکی ندارد) ── */}
|
||||
{!myLoading && !my && !usedTrial && (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #eff6ff, #f5f3ff)',
|
||||
border: '1.5px dashed #93c5fd',
|
||||
background: 'linear-gradient(135deg, var(--info-bg), var(--violet-bg))',
|
||||
border: '1.5px dashed color-mix(in oklch, var(--info) 50%, transparent)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
padding: '18px 22px',
|
||||
marginBottom: 24,
|
||||
display: 'flex', alignItems: 'center', gap: 14,
|
||||
}}>
|
||||
<SparklesIcon style={{ width: 28, color: '#2563eb', flexShrink: 0 }} />
|
||||
<SparklesIcon style={{ width: 28, color: 'var(--info)', flexShrink: 0 }} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 15, color: 'var(--text)', marginBottom: 2 }}>
|
||||
یک ماه تریال رایگان دارید!
|
||||
@@ -307,16 +307,16 @@ export default function SubscriptionPage() {
|
||||
{/* انتخاب درگاه */}
|
||||
{isTestMode ? (
|
||||
<div style={{
|
||||
background: '#fef9c3',
|
||||
border: '1px solid #fbbf24',
|
||||
background: 'var(--warning-bg)',
|
||||
border: '1px solid var(--warning)',
|
||||
borderRadius: 10,
|
||||
padding: '12px 16px',
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
}}>
|
||||
<span style={{ fontSize: 20 }}>⚠️</span>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700, fontSize: 13.5, color: '#92400e' }}>درگاه آزمایشی فعال است</div>
|
||||
<div style={{ fontSize: 12, color: '#b45309', marginTop: 2 }}>پول واقعی کسر نخواهد شد — این تراکنش آزمایشی است</div>
|
||||
<div style={{ fontWeight: 700, fontSize: 13.5, color: 'var(--warning)' }}>درگاه آزمایشی فعال است</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--warning)', marginTop: 2, opacity: 0.8 }}>پول واقعی کسر نخواهد شد — این تراکنش آزمایشی است</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -475,10 +475,10 @@ function PlanCard({
|
||||
<span style={{
|
||||
width: 20, height: 20, borderRadius: '50%', flexShrink: 0,
|
||||
display: 'grid', placeItems: 'center',
|
||||
background: enabled ? '#dcfce7' : 'var(--surface-3)',
|
||||
background: enabled ? 'var(--success-bg)' : 'var(--surface-3)',
|
||||
}}>
|
||||
{enabled
|
||||
? <CheckIcon style={{ width: 12, color: '#16a34a' }} />
|
||||
? <CheckIcon style={{ width: 12, color: 'var(--success)' }} />
|
||||
: <XMarkIcon style={{ width: 11, color: 'var(--text-3)' }} />
|
||||
}
|
||||
</span>
|
||||
@@ -493,8 +493,8 @@ function PlanCard({
|
||||
{trialPeriod && !usedTrial && plan.level > 0 && !isDowngrade && (
|
||||
<div style={{
|
||||
marginTop: 14, padding: '10px 12px', borderRadius: 'var(--r-sm)',
|
||||
background: 'linear-gradient(135deg, #eff6ff, #f5f3ff)',
|
||||
border: '1px dashed #93c5fd', fontSize: 12.5,
|
||||
background: 'linear-gradient(135deg, var(--info-bg), var(--violet-bg))',
|
||||
border: '1px dashed color-mix(in oklch, var(--info) 50%, transparent)', fontSize: 12.5,
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
}}>
|
||||
<span>🎁</span>
|
||||
@@ -520,8 +520,8 @@ function PlanCard({
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '10px 12px', borderRadius: 'var(--r-sm)',
|
||||
background: '#fef9c3', border: '1px solid #fbbf24',
|
||||
fontSize: 12.5, color: '#92400e', marginBottom: 2,
|
||||
background: 'var(--warning-bg)', border: '1px solid var(--warning)',
|
||||
fontSize: 12.5, color: 'var(--warning)', marginBottom: 2,
|
||||
}}>
|
||||
<span style={{ fontSize: 15 }}>🔒</span>
|
||||
<span>تا پایان اشتراک فعلی قابل انتخاب نیست</span>
|
||||
|
||||
Reference in New Issue
Block a user