- Added seed_realistic_data.php to clean existing data and populate the database with realistic entries for doctors, clinics, and secretaries. - Created a structured approach to generate 100 doctors per city with diverse specialties and services. - Implemented database cleanup routines to ensure a fresh start for data seeding. - Enhanced the DoctorSecretaryRepository with improved comments for clarity.
396 lines
18 KiB
Markdown
396 lines
18 KiB
Markdown
# بازطراحی UI/UX پنج صفحه کلینیکپرو
|
||
|
||
## زمینه
|
||
|
||
پروژه ClinicPro یک پنل مدیریت کلینیک است با React 19 + TypeScript و یک سیستم CSS دیزاین اختصاصی. SubscriptionPage اخیراً بازطراحی شده و الگویی ایجاد کرده که بقیه صفحات باید با آن همراستا شوند. ۵ صفحه هنوز از ساختار قدیمی استفاده میکنند: جدولهای ساده، کارتهای بدون سلسلهمراتب بصری، و فرمهای بدون هویت. هدف این است که هر صفحه را به یک تجربه منسجم، مدرن و RTL-first تبدیل کنیم.
|
||
|
||
## سیستم CSS موجود
|
||
|
||
متغیرها و کلاسهای قابل استفاده (در `assets/admin/styles.css`):
|
||
|
||
```css
|
||
/* متغیرها */
|
||
--primary: oklch(0.55 0.2 256) --primary-subtle: oklch(0.97 0.02 256)
|
||
--surface: #fff --border: oklch(0.9 0 0) --text-1 / --text-2 / --text-3 --r
|
||
(border-radius base) --gap / --card-pad /* کلاسهای کاربردی */ .card
|
||
/* white surface با shadow و border-radius */ .card-pad
|
||
/* padding داخل card */ .stat /* عدد آماری بزرگ + label */ .stat-grid
|
||
/* grid چند ستونه برای statها */ .badge green/red/yellow/blue/gray
|
||
/* برچسب وضعیت */ .btn primary/sm/ghost /* دکمه */ .field
|
||
/* label + input wrapper */ .seg /* segment/tab selector */ .bar
|
||
/* نوار پیشرفت */ .grid-2 /* دوستونه responsive */ .avatar sm
|
||
/* آواتار دایرهای */;
|
||
```
|
||
|
||
**الگوی header صفحات:** `<PageHeader title="..." description="..." action={<button>} />`
|
||
|
||
## فایلهای مرتبط
|
||
|
||
| فایل | نقش |
|
||
| ------------------------------------------- | -------------------------------------------------------- |
|
||
| `assets/admin/pages/MyPatientsPage.tsx` | پرونده بیماران — دو حالت: لیست + جزئیات بیمار |
|
||
| `assets/admin/pages/StaffPage.tsx` | مدیریت پرسنل — CRUD |
|
||
| `assets/admin/pages/MySecretariesPage.tsx` | مدیریت منشی ها — CRUD + permissions matrix |
|
||
| `assets/admin/pages/ClinicServicesPage.tsx` | سرویسهای کلینیک — دو پنل: بخشها + آیتمها |
|
||
| `assets/admin/pages/SmsWalletPage.tsx` | کیف پول پیامک — balance + settings + logs |
|
||
| `assets/admin/styles.css` | سیستم CSS کامل |
|
||
| `assets/admin/pages/SubscriptionPage.tsx` | **مرجع طراحی** — بازطراحیشده با gradient + progress bar |
|
||
|
||
## وضعیت فعلی هر صفحه
|
||
|
||
### ۱. MyPatientsPage
|
||
|
||
دو حالت: حالت لیست و حالت پرونده بیمار.
|
||
|
||
**حالت لیست:** یک `.card` با `<DataTable>` ساده + search input بدون styling در header.
|
||
|
||
**حالت پرونده بیمار:** `PageHeader` با نام + برگشت. یک `.card` با `sessions.map(SessionRow)`. Modal ثبت مراجعه با سهستونه برای قیمت/بیمه/روش پرداخت.
|
||
|
||
```tsx
|
||
// لیست ساده فعلی
|
||
<div className="card">
|
||
<div style={{ padding: '12px 16px 0', display: 'flex', gap: 8 }}>
|
||
<div style={{ position: 'relative', flex: 1 }}>
|
||
<MagnifyingGlassIcon ... />
|
||
<input ... placeholder="جستجو بر اساس نام یا تلفن..." />
|
||
</div>
|
||
</div>
|
||
<DataTable columns={recordColumns} data={records} loading={isLoading} ... />
|
||
<Pagination ... />
|
||
</div>
|
||
```
|
||
|
||
**ستونهای لیست بیماران:** آواتار + نام + تلفن، تاریخ ثبت، دکمه «پرونده».
|
||
**SessionRow:** هنوز خوانده نشده — احتمالاً نمایش تجمیعی اطلاعات مراجعه.
|
||
|
||
### ۲. StaffPage
|
||
|
||
```tsx
|
||
// فعلی — جدول + دو modal
|
||
<PageHeader title="مدیریت پرسنل" ... action={<button>افزودن پرسنل</button>} />
|
||
<div className="card">
|
||
<DataTable columns={columns} data={staff} loading={isLoading} />
|
||
</div>
|
||
// Modal ایجاد و ویرایش با StaffFormFields (فیلدهای: full_name, phone, job_title, address, national_code)
|
||
```
|
||
|
||
ستونها: نام، سمت، تلفن، کد ملی، وضعیت (ActiveBadge)، تاریخ ثبت، عملیات.
|
||
هیچ KPI یا آمار مقدماتی ندارد.
|
||
|
||
### ۳. MySecretariesPage
|
||
|
||
```tsx
|
||
// جدول دستی (نه DataTable)
|
||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13.5 }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: '1px solid var(--border)' }}>
|
||
<th style={{ textAlign: 'right', padding: '10px 16px', color: 'var(--text-3)', fontWeight: 500 }}>نام</th>
|
||
...
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{secretaries.map((s, i) => (
|
||
<tr key={s.uuid} style={{ borderBottom: ... }}>
|
||
<td style={{ padding: '10px 16px' }}><b>{s.user_name}</b></td>
|
||
...
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
```
|
||
|
||
PermissionsMatrix: جدول checkbox با ۶ ستون برای ۴ بخش — دادهمحور و قابل استفاده اما بصری ضعیف.
|
||
|
||
حالت Empty state: فقط یک div با متن `هنوز منشیای ثبت نشده است`.
|
||
|
||
### ۴. ClinicServicesPage
|
||
|
||
```tsx
|
||
// دو پنل side-by-side
|
||
<div style={{ display: 'grid', gridTemplateColumns: '280px 1fr', gap: 16, alignItems: 'start' }}>
|
||
{/* پنل بخشها */}
|
||
<div className="card">
|
||
<div style={{ display: 'flex', ... marginBottom: 12 }}>
|
||
<b style={{ fontSize: 14 }}>بخشها</b>
|
||
<button className="btn primary sm"><PlusIcon /></button>
|
||
</div>
|
||
{sections.map((s) => (
|
||
<div style={{ padding: '8px 10px', borderRadius: 6, cursor: 'pointer', background: selected ? 'var(--primary-subtle)' : ... }}>
|
||
...
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* پنل آیتمها */}
|
||
<div className="card">
|
||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
||
...
|
||
</table>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
مشکل: بخشها فقط text + دو آیکون هستند. active/inactive badge ندارند. جدول آیتمها خیلی ساده است.
|
||
|
||
### ۵. SmsWalletPage
|
||
|
||
```tsx
|
||
// دو کارت بالا
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 20 }}>
|
||
{/* موجودی */}
|
||
<div className="card">
|
||
<div style={{ fontSize: 13, color: 'var(--text-3)', marginBottom: 4 }}>موجودی کیف پول</div>
|
||
<div style={{ fontSize: 28, fontWeight: 700 }}>{formatRial(balance?.balance_rials ?? 0)}</div>
|
||
<div style={{ fontSize: 13, color: 'var(--text-3)' }}>معادل {formatNumber(...)} پیامک</div>
|
||
<button className="btn primary sm">شارژ کیف پول</button>
|
||
</div>
|
||
{/* تنظیمات */}
|
||
<div className="card">...</div>
|
||
</div>
|
||
|
||
{/* جدول تراکنشها */}
|
||
<div className="card">
|
||
<table style={{ ... fontSize: 13 }}>...</table>
|
||
</div>
|
||
```
|
||
|
||
کارت موجودی: عدد بزرگ اما بدون visual hierarchy. نوار پیشرفت و badge «موجودی» ندارد.
|
||
|
||
## وظایف
|
||
|
||
### ۱. بازطراحی MyPatientsPage
|
||
|
||
**هدف:** لیست با identity واضح + پرونده بیمار با مینیداشبورد.
|
||
|
||
**الف — حالت لیست بیماران:**
|
||
|
||
- Header اختصاصی: `<PageHeader>` با description «مراجعهکنندگان ثبتشده شما»
|
||
- Search bar را با یک wrapper card جداگانه به یک toolbar تبدیل کن (نه داخل card جدول)
|
||
- `DataTable` را با ردیفهای بهتر جایگزین کن: هر ردیف آواتار gradient + نام بولد + شماره با آیکون تلفن + تاریخ ثبت شمسی
|
||
- دکمه «پرونده» به آیکون `FolderOpenIcon` + متن «مشاهده پرونده» تبدیل شود (btn primary sm)
|
||
- وقتی بیماری نیست: Empty State با آیکون بزرگ + متن راهنما
|
||
|
||
**ب — حالت پرونده بیمار (selectedRecord):**
|
||
|
||
- PageHeader با نام بیمار + شماره + تاریخ ثبت
|
||
- یک بنر/info-card بالای صفحه: نام، تلفن، تعداد مراجعات (از `totalSes`)
|
||
- SessionRow باید بهتر شود — هر مراجعه یک card مستقل با:
|
||
- سربرگ: تاریخ شمسی + روش پرداخت به عنوان badge
|
||
- بدنه: قیمت ویزیت + تخفیف بیمه + جمع خدمات + قیمت نهایی در یک `.grid-2` یا flex row
|
||
- footer: لیست خدمات (services) اگر وجود داشت
|
||
- دکمه «ویرایش» در گوشه بالا سمت چپ کارت
|
||
|
||
**ج — Modal ثبت مراجعه:**
|
||
|
||
- سه فیلد قیمت/بیمه پایه/بیمه تکمیلی را در یک `.grid-2` یا grid سهستونه گذاشتن خوب است — این کافیست
|
||
- بخش «خدمات» را با یک separator بصری از فیلدهای اصلی جدا کن
|
||
- لیست خدمات انتخابشده را با badge یا chip نشان بده (نه فقط متن)
|
||
|
||
**نکته:** SessionRow را کامل بخوان قبل از پیادهسازی.
|
||
|
||
---
|
||
|
||
### ۲. بازطراحی StaffPage
|
||
|
||
**هدف:** صفحهای با KPI bar + جدول بهتر + فرم واضحتر.
|
||
|
||
**الف — KPI bar:**
|
||
|
||
- بالای جدول، یک `.stat-grid` با ۳ card: تعداد پرسنل کل، تعداد فعال، تعداد غیرفعال
|
||
- داده از همان `staff` query محاسبه شود (نه API جدید):
|
||
```ts
|
||
const total = staff.length;
|
||
const active = staff.filter((s) => s.active).length;
|
||
const inactive = total - active;
|
||
```
|
||
|
||
**ب — جدول بهتر:**
|
||
|
||
- ستون «نام» را بهبود بده: `full_name` + `job_title` زیرش (مثل MyPatientsPage)
|
||
- ستون «تلفن» و «کد ملی» را در موبایل پنهان کن (CSS media query اگر لازم بود)
|
||
- ردیفهای hover state با background تغییر کند
|
||
|
||
**ج — فرم StaffFormFields:**
|
||
|
||
- فیلدهای `full_name` و `job_title` را در یک ردیف (`.grid-2`) قرار بده
|
||
- فیلدهای `phone` و `national_code` را در یک ردیف دیگر
|
||
- فیلد `address` تنها بماند (textarea یا input تمام عرض)
|
||
|
||
---
|
||
|
||
### ۳. بازطراحی MySecretariesPage
|
||
|
||
**هدف:** جدول دستی را به DataTable شیک تبدیل کن + PermissionsMatrix بصریتر + Empty State جذاب.
|
||
|
||
**الف — جدول منشی ها:**
|
||
|
||
- جدول دستی را **حذف کن** و از `DataTable` component استفاده کن
|
||
- ستون «نام»: آواتار کوچک (initial از `user_name`) + نام بولد
|
||
- ستون «موبایل»: با `maskMobile()` و `dir="ltr"` + آیکون تلفن
|
||
- ستون «وضعیت»: `<ActiveBadge>`
|
||
- ستون «تاریخ»: `formatDate()`
|
||
- ستون «عملیات»: دو دکمه `sm` (ویرایش دسترسیها + غیرفعالسازی)
|
||
|
||
**ب — Empty State:**
|
||
|
||
- وقتی `secretaries.length === 0`:
|
||
```tsx
|
||
<div
|
||
className="card card-pad"
|
||
style={{ textAlign: "center", padding: "60px 24px" }}
|
||
>
|
||
<IdentificationIcon
|
||
style={{ width: 48, color: "var(--text-3)", margin: "0 auto 16px" }}
|
||
/>
|
||
<div style={{ fontWeight: 600, marginBottom: 8 }}>
|
||
هنوز منشیای اضافه نشده
|
||
</div>
|
||
<div
|
||
style={{ color: "var(--text-3)", fontSize: 13.5, marginBottom: 20 }}
|
||
>
|
||
منشی میتواند نوبتها و اطلاعات کلینیک را مدیریت کند
|
||
</div>
|
||
<button className="btn primary sm" onClick={() => setCreateOpen(true)}>
|
||
<PlusIcon style={{ width: 16 }} /> افزودن اولین منشی
|
||
</button>
|
||
</div>
|
||
```
|
||
|
||
**ج — PermissionsMatrix:**
|
||
|
||
- ردیفهای جدول با رنگ متناوب (striped) یا hover state
|
||
- checkboxها با `accent-color: var(--primary)` (قبلاً بود، نگه دار)
|
||
- header ستونها را bold و با background کمرنگ کن
|
||
- برای cellهای `—` (N/A) رنگ `var(--border)` خوب است
|
||
|
||
---
|
||
|
||
### ۴. بازطراحی ClinicServicesPage
|
||
|
||
**هدف:** پنل بخشها بهتر + جدول آیتمها با badge فعال/غیرفعال.
|
||
|
||
**الف — پنل بخشها (ستون ۲۸۰px):**
|
||
|
||
- هر بخش یک item با:
|
||
- آیکون `WrenchScrewdriverIcon` در سمت راست (color از theme)
|
||
- نام بخش bold
|
||
- badge «فعال» / «غیرفعال» بر اساس `s.active`
|
||
- دکمههای edit/delete فقط هنگام hover نشان داده شوند (opacity transition)
|
||
- انتخاب شده: border `var(--primary)` + background `var(--primary-subtle)`
|
||
- افزودن بخش: دکمه با آیکون + متن «بخش جدید» (نه فقط آیکون)
|
||
|
||
**ب — پنل آیتمها (ستون 1fr):**
|
||
|
||
- بالای جدول یک toolbar: نام بخش به عنوان breadcrumb + تعداد آیتمها + دکمه «سرویس جدید»
|
||
- هر row آیتم:
|
||
- badge «فعال» / «غیرفعال» برای `item.active`
|
||
- قیمت با `formatRial()` و رنگ primary
|
||
- نام پرسنل با avatar خیلی کوچک اگر وجود داشت
|
||
- Empty state وقتی `items.length === 0`:
|
||
```tsx
|
||
<div style={{ textAlign: 'center', padding: '48px 0', color: 'var(--text-3)' }}>
|
||
<PlusIcon style={{ width: 32, margin: '0 auto 12px', display: 'block' }} />
|
||
سرویسی در این بخش وجود ندارد
|
||
<br />
|
||
<button className="btn primary sm" style={{ marginTop: 12 }} onClick={...}>افزودن سرویس</button>
|
||
</div>
|
||
```
|
||
|
||
**ج — State اولیه (هیچ بخشی انتخاب نشده):**
|
||
|
||
- جای پیام «یک بخش را از سمت راست انتخاب کنید» یک آنیمیشن ساده یا placeholder card با رنگ dashed border قرار بده:
|
||
```tsx
|
||
<div
|
||
style={{
|
||
border: "2px dashed var(--border)",
|
||
borderRadius: 10,
|
||
padding: "60px 0",
|
||
textAlign: "center",
|
||
color: "var(--text-3)",
|
||
}}
|
||
>
|
||
<WrenchScrewdriverIcon style={{ width: 40, margin: "0 auto 12px" }} />
|
||
<div>یک بخش انتخاب کنید تا سرویسهای آن را ببینید</div>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
### ۵. بازطراحی SmsWalletPage
|
||
|
||
**هدف:** کارت موجودی با visual hierarchy قوی + تنظیمات واضحتر + جدول تراکنشها با visual type indicator.
|
||
|
||
**الف — کارت موجودی:**
|
||
|
||
- gradient background (مثل SubscriptionPage): `background: linear-gradient(135deg, oklch(0.52 0.22 256), oklch(0.40 0.18 256))`
|
||
- رنگ متن: white
|
||
- موجودی را بزرگتر کن: `fontSize: 36, fontWeight: 800`
|
||
- زیر موجودی: نوار پیشرفت `.bar` که نشان میدهد چند پیامک باقیمانده (نسبت به یک سقف فرضی مثلاً ۵۰۰ پیامک یا بر اساس estimated_sms_count)
|
||
- badge status: اگر `estimated_sms_count < 10` → badge قرمز «موجودی کم»، اگر ≥ 10 → badge سبز «فعال»
|
||
- دکمه شارژ: white با primary text (برعکس از theme معمول)
|
||
|
||
**ب — کارت تنظیمات:**
|
||
|
||
- عنوان با divider پایین
|
||
- هر toggle را با یک card داخلی wrapper کن:
|
||
```tsx
|
||
<div style={{ background: 'var(--primary-subtle)', borderRadius: 8, padding: '10px 14px', display: 'flex', alignItems: 'center', gap: 10 }}>
|
||
<input type="checkbox" ... />
|
||
<div>
|
||
<div style={{ fontWeight: 600, fontSize: 13.5 }}>یادآوری قبل از نوبت</div>
|
||
<div style={{ color: 'var(--text-3)', fontSize: 12 }}>ارسال خودکار چند ساعت قبل از نوبت</div>
|
||
</div>
|
||
</div>
|
||
```
|
||
- input «چند ساعت قبل» را با border و padding واضحتر style کن
|
||
|
||
**ج — جدول تراکنشها:**
|
||
|
||
- ستون «نوع» را با یک آیکون بهبود بده:
|
||
- `credit`: `ArrowUpCircleIcon` با رنگ سبز
|
||
- `debit`: `ArrowDownCircleIcon` با رنگ قرمز
|
||
- ستون «مبلغ» را بر اساس نوع رنگ بده: credit → سبز، debit → قرمز
|
||
- header table با background خاکستری کمرنگ
|
||
|
||
**د — Modal شارژ:**
|
||
|
||
- انتخاب gateway را به radio card تبدیل کن (مثل SubscriptionPage):
|
||
```tsx
|
||
{
|
||
(["mellat", "sep"] as const).map((gw) => (
|
||
<div
|
||
key={gw}
|
||
onClick={() => setGateway(gw)}
|
||
style={{
|
||
border: `2px solid ${gateway === gw ? "var(--primary)" : "var(--border)"}`,
|
||
borderRadius: 8,
|
||
padding: "10px 14px",
|
||
cursor: "pointer",
|
||
background: gateway === gw ? "var(--primary-subtle)" : "",
|
||
transition: "all 0.15s",
|
||
flex: 1,
|
||
textAlign: "center",
|
||
}}
|
||
>
|
||
<div style={{ fontWeight: 600 }}>
|
||
{gw === "mellat" ? "بانک ملت" : "سپ"}
|
||
</div>
|
||
</div>
|
||
));
|
||
}
|
||
```
|
||
- پیشنمایش مبلغ: وقتی کاربر عدد میزند، زیر input بنویس «پرداخت X ریال از طریق Y»
|
||
|
||
## نکات مهم
|
||
|
||
1. **فقط فرانتاند** — هیچ endpoint، migration، یا تغییر backend لازم نیست
|
||
2. **CSS موجود را استفاده کن** — هیچ کتابخانه جدید اضافه نکن. inline style برای تنظیمات جزئی قابل قبول است
|
||
3. **DataTable component موجود** در `assets/admin/components/ui/DataTable.tsx` — برای جداول از آن استفاده کن (ترجیحاً بر inline table)
|
||
4. **RTL** رعایت شود — padding/margin/icon placement همه RTL-first باشد
|
||
5. **هر صفحه مستقل** پیادهسازی شود — نه refactor مشترک
|
||
6. **SubscriptionPage** را به عنوان مرجع visual نگه دار — gradient، progress bar، و card layout آن مدل است
|
||
7. **بعد از هر صفحه** `ddev exec yarn dev` اجرا کن تا TypeScript error نداشته باشد
|
||
8. **SessionRow** در MyPatientsPage: قبل از ویرایش، آن component را کامل بخوان (احتمالاً inline در همان فایل است)
|
||
9. **`maskMobile()`** از `../lib/utils` — برای نمایش شماره موبایل منشی ها استفاده کن
|
||
10. **`formatDate()` و `formatDateTime()`** — همه تاریخها شمسی نمایش داده شوند
|