diff --git a/.claude/prompt/ui-ux-pages-redesign.md b/.claude/prompt/ui-ux-pages-redesign.md new file mode 100644 index 00000000..32bb424e --- /dev/null +++ b/.claude/prompt/ui-ux-pages-redesign.md @@ -0,0 +1,368 @@ +# بازطراحی 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 صفحات:** `} />` + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `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` با `` ساده + search input بدون styling در header. + +**حالت پرونده بیمار:** `PageHeader` با نام + برگشت. یک `.card` با `sessions.map(SessionRow)`. Modal ثبت مراجعه با سه‌ستونه برای قیمت/بیمه/روش پرداخت. + +```tsx +// لیست ساده فعلی +
+
+
+ + +
+
+ + +
+``` + +**ستون‌های لیست بیماران:** آواتار + نام + تلفن، تاریخ ثبت، دکمه «پرونده». +**SessionRow:** هنوز خوانده نشده — احتمالاً نمایش تجمیعی اطلاعات مراجعه. + +### ۲. StaffPage + +```tsx +// فعلی — جدول + دو modal +افزودن پرسنل} /> +
+ +
+// Modal ایجاد و ویرایش با StaffFormFields (فیلدهای: full_name, phone, job_title, address, national_code) +``` + +ستون‌ها: نام، سمت، تلفن، کد ملی، وضعیت (ActiveBadge)، تاریخ ثبت، عملیات. +هیچ KPI یا آمار مقدماتی ندارد. + +### ۳. MySecretariesPage + +```tsx +// جدول دستی (نه DataTable) + + + + + ... + + + + {secretaries.map((s, i) => ( + + + ... + + ))} + +
نام
{s.user_name}
+``` + +PermissionsMatrix: جدول checkbox با ۶ ستون برای ۴ بخش — داده‌محور و قابل استفاده اما بصری ضعیف. + +حالت Empty state: فقط یک div با متن `هنوز منشی‌ای ثبت نشده است`. + +### ۴. ClinicServicesPage + +```tsx +// دو پنل side-by-side +
+ {/* پنل بخش‌ها */} +
+
+ بخش‌ها + +
+ {sections.map((s) => ( +
+ ... +
+ ))} +
+ + {/* پنل آیتم‌ها */} +
+ + ... +
+
+
+``` + +مشکل: بخش‌ها فقط text + دو آیکون هستند. active/inactive badge ندارند. جدول آیتم‌ها خیلی ساده است. + +### ۵. SmsWalletPage + +```tsx +// دو کارت بالا +
+ {/* موجودی */} +
+
موجودی کیف پول
+
{formatRial(balance?.balance_rials ?? 0)}
+
معادل {formatNumber(...)} پیامک
+ +
+ {/* تنظیمات */} +
...
+
+ +{/* جدول تراکنش‌ها */} +
+ ...
+
+``` + +کارت موجودی: عدد بزرگ اما بدون visual hierarchy. نوار پیشرفت و badge «موجودی» ندارد. + +## وظایف + +### ۱. بازطراحی MyPatientsPage + +**هدف:** لیست با identity واضح + پرونده بیمار با مینی‌داشبورد. + +**الف — حالت لیست بیماران:** +- Header اختصاصی: `` با 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"` + آیکون تلفن +- ستون «وضعیت»: `` +- ستون «تاریخ»: `formatDate()` +- ستون «عملیات»: دو دکمه `sm` (ویرایش دسترسی‌ها + غیرفعال‌سازی) + +**ب — Empty State:** +- وقتی `secretaries.length === 0`: + ```tsx +
+ +
هنوز منشی‌ای اضافه نشده
+
+ منشی می‌تواند نوبت‌ها و اطلاعات کلینیک را مدیریت کند +
+ +
+ ``` + +**ج — 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 +
+ + سرویسی در این بخش وجود ندارد +
+ +
+ ``` + +**ج — State اولیه (هیچ بخشی انتخاب نشده):** +- جای پیام «یک بخش را از سمت راست انتخاب کنید» یک آنیمیشن ساده یا placeholder card با رنگ dashed border قرار بده: + ```tsx +
+ +
یک بخش انتخاب کنید تا سرویس‌های آن را ببینید
+
+ ``` + +--- + +### ۵. بازطراحی 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 +
+ +
+
یادآوری قبل از نوبت
+
ارسال خودکار چند ساعت قبل از نوبت
+
+
+ ``` +- 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) => ( +
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', + }} + > +
{gw === 'mellat' ? 'بانک ملت' : 'سپ'}
+
+ ))} + ``` +- پیش‌نمایش مبلغ: وقتی کاربر عدد می‌زند، زیر 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()`** — همه تاریخ‌ها شمسی نمایش داده شوند diff --git a/assets/admin/pages/ClinicServicesPage.tsx b/assets/admin/pages/ClinicServicesPage.tsx index d768ee5b..e5bc965f 100644 --- a/assets/admin/pages/ClinicServicesPage.tsx +++ b/assets/admin/pages/ClinicServicesPage.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { PlusIcon, PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; +import { PlusIcon, PencilIcon, TrashIcon, WrenchScrewdriverIcon } from '@heroicons/react/24/outline'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; @@ -129,84 +129,177 @@ export default function ClinicServicesPage() { <> -
+
{/* ستون بخش‌ها */}
-
+
بخش‌ها -
- {sectionsLoading ? ( -
در حال بارگذاری...
- ) : sections.length === 0 ? ( -
بخشی ثبت نشده است
- ) : ( -
- {sections.map((s) => ( + +
+ {sectionsLoading ? ( +
در حال بارگذاری...
+ ) : sections.length === 0 ? ( +
+ +
بخشی ثبت نشده است
+
+ ) : ( + sections.map((s) => (
setSelectedSection(s)} > - {s.name} +
+ + + {s.name} + +
e.stopPropagation()}> - - + +
- ))} -
- )} + )) + )} +
{/* ستون آیتم‌ها */}
{!selectedSection ? ( -
- یک بخش را از سمت راست انتخاب کنید +
+ +
یک بخش انتخاب کنید
+
تا سرویس‌های آن را مشاهده و مدیریت کنید
) : ( <> -
- سرویس‌های «{selectedSection.name}» + {/* toolbar بخش انتخاب‌شده */} +
+
+ + {selectedSection.name} + {!itemsLoading && ( + {items.length} سرویس + )} +
+ {itemsLoading ? ( -
در حال بارگذاری...
+
در حال بارگذاری...
) : items.length === 0 ? ( -
سرویسی ثبت نشده است
+
+ +
سرویسی در این بخش وجود ندارد
+ +
) : ( - - - - - + + + + + - {items.map((item) => ( - - - - - + + + + diff --git a/assets/admin/pages/MyPatientsPage.tsx b/assets/admin/pages/MyPatientsPage.tsx index 0e437b49..30fb1601 100644 --- a/assets/admin/pages/MyPatientsPage.tsx +++ b/assets/admin/pages/MyPatientsPage.tsx @@ -2,6 +2,7 @@ import React, { useState, useCallback } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { MagnifyingGlassIcon, PlusIcon, ChevronRightIcon, PencilIcon, PhoneIcon, + FolderOpenIcon, UsersIcon, } from '@heroicons/react/24/outline'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -30,6 +31,10 @@ const PAYMENT_LABELS: Record = { cash: 'نقدی', card: 'کارت', insurance: 'بیمه', online: 'آنلاین', pending: 'در انتظار', }; +const PAYMENT_BADGE: Record = { + cash: 'green', card: 'blue', insurance: 'purple', online: 'blue', pending: 'amber', +}; + const EMPTY_RECORDS: PatientRecord[] = []; const EMPTY_SESSIONS: PatientSession[] = []; @@ -139,13 +144,20 @@ export default function MyPatientsPage() { key: 'user', header: 'بیمار', render: (r) => ( -
-
+
+
{(r.user.fullName ?? '?').charAt(0)}
{r.user.fullName ?? '—'}
-
+
{r.user.phone ?? '—'}
@@ -153,13 +165,24 @@ export default function MyPatientsPage() {
), }, - { key: 'created_at', header: 'تاریخ ثبت', render: (r) => formatDate(r.created_at) }, + { + key: 'created_at', + header: 'تاریخ ثبت', + render: (r) => ( + {formatDate(r.created_at)} + ), + }, { key: 'uuid', - header: 'عملیات', + header: '', render: (r) => ( - ), }, @@ -168,23 +191,47 @@ export default function MyPatientsPage() { if (!selectedRecord) { return ( <> - + + +
+
+ + handleSearch(e.target.value)} + placeholder="جستجو بر اساس نام یا تلفن..." + /> +
+
+
-
-
- - handleSearch(e.target.value)} - placeholder="جستجو بر اساس نام یا تلفن..." - /> + {records.length === 0 && !isLoading ? ( +
+ +
+ {search ? 'بیماری یافت نشد' : 'هنوز بیماری ثبت نشده است'} +
+
+ {search ? 'عبارت جستجو را تغییر دهید' : 'پس از ثبت نوبت، پرونده بیمار به طور خودکار ایجاد می‌شود'} +
-
- -
- -
+ ) : ( + <> + +
+ +
+ + )}
); @@ -200,19 +247,66 @@ export default function MyPatientsPage() { -
} /> + {/* بنر اطلاعات بیمار */} +
+
+ {(selectedRecord.user.fullName ?? '?').charAt(0)} +
+
+
{selectedRecord.user.fullName ?? '—'}
+
+ + {selectedRecord.user.phone ?? '—'} +
+
+
+
{totalSes}
+
مراجعه
+
+
+
-
مراجعات
+
+ تاریخچه مراجعات +
{sessionsLoading ? ( -
در حال بارگذاری...
+
در حال بارگذاری...
) : sessions.length === 0 ? ( -
مراجعه‌ای ثبت نشده است
+
+ +
مراجعه‌ای ثبت نشده است
+ +
) : ( sessions.map((s) => ) )} @@ -226,7 +320,7 @@ export default function MyPatientsPage() { {/* Modal مراجعه جدید */} setSessionModal(false)} title="ثبت مراجعه جدید">
-
+
@@ -255,8 +349,8 @@ export default function MyPatientsPage() {
-
افزودن سرویس
-
+
افزودن خدمات
+
-
- {selectedServices.map((svc) => ( -
- {svc.name} -
- {formatRial(svc.price_rials)} - -
+ + {selectedServices.length > 0 && ( +
+ {selectedServices.map((svc) => ( +
+ {svc.name} + {formatRial(svc.price_rials)} + +
+ ))}
- ))} + )}
-
-
- جمع سرویس‌ها +
+
+ جمع خدمات {formatRial(servicesTotal)}
@@ -325,34 +430,69 @@ export default function MyPatientsPage() { function SessionRow({ session, onEdit }: { session: PatientSession; onEdit: (s: PatientSession) => void }) { const [open, setOpen] = useState(false); + const badgeColor = PAYMENT_BADGE[session.payment_method] ?? 'gray'; + return ( -
+
setOpen(!open)} > -
+
+
{formatDateTime(session.created_at)} - + {PAYMENT_LABELS[session.payment_method] ?? session.payment_method} - {formatRial(session.final_price_rials)} + {formatRial(session.final_price_rials)}
-
+ {open && ( -
-
قیمت ویزیت
{formatRial(session.visit_price_rials)}
-
تخفیف بیمه پایه
{formatNumber(parseFloat(session.base_insurance_discount_percent))}٪
-
جمع سرویس‌ها
{formatRial(session.services_total_rials)}
- {session.notes && ( -
-
یادداشت
-
{session.notes}
+
+
+
+
قیمت ویزیت
+
{formatRial(session.visit_price_rials)}
- )} +
+
تخفیف بیمه پایه
+
{formatNumber(parseFloat(session.base_insurance_discount_percent))}٪
+
+
+
جمع خدمات
+
{formatRial(session.services_total_rials)}
+
+ {session.notes && ( +
+
یادداشت
+
{session.notes}
+
+ )} +
)}
@@ -390,7 +530,11 @@ function EditSessionModal({
نام سرویسقیمتپرسنل مسئولعملیات
نام سرویسقیمتپرسنل مسئولعملیات
{item.name}{formatRial(item.price_rials)}{item.staff?.full_name ?? '—'} + {items.map((item, idx) => ( +
{item.name} + {formatRial(item.price_rials)} + + {item.staff ? ( +
+
+ {item.staff.full_name.charAt(0)} +
+ {item.staff.full_name} +
+ ) : ( + + )} +
- - + +