feat: update UI components to use CSS variables for dark mode compatibility

This commit is contained in:
hamed
2026-06-15 14:50:45 +03:30
parent df7a784701
commit e95c5c12d1
8 changed files with 260 additions and 73 deletions
+186
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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 دیتای واقعی دکتران و کلینیک:
+23 -23
View File
@@ -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>
+2 -2
View File
@@ -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',
}}>
حساب شما دسترسی به پنل مدیریت را ندارد
+8 -8
View File
@@ -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>
+16 -16
View File
@@ -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))}`
+16 -16
View File
@@ -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>