- 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.
947 lines
27 KiB
Markdown
947 lines
27 KiB
Markdown
# پیادهسازی فرانتاند فاز ۲ — TASK-10 تا TASK-16
|
||
|
||
## زمینه
|
||
|
||
بکاند فاز ۲ کاملاً پیادهسازی و تست شده است. این پرامپت فقط فرانتاند ۷ تسک را پیادهسازی میکند.
|
||
هر تسک که تمام شد → `git commit` بزن (message فارسی مختصر).
|
||
بعد از هر تسک بلافاصله `yarn dev` اجرا کن تا خطاهای TypeScript مشخص شوند.
|
||
|
||
## قوانین ثابت پروژه
|
||
|
||
- **Paginated**: items از `data?.data`، total از `data?.meta?.totalRecords`
|
||
- **Single resource**: از `data?.data` (ممکن است double-nested باشد — `data?.data?.data`)
|
||
- **Category API**: triple-nested → `data?.data?.data ?? []`
|
||
- **تاریخ نمایش**: همیشه با `formatDate()` یا `formatDateTime()` از `lib/utils`
|
||
- **Forms**: React Hook Form + Zod resolver
|
||
- **Data fetching**: TanStack Query v5 (`useQuery` / `useMutation`)
|
||
- **UI**: از `DataTable`, `Pagination`, `Modal`, `ConfirmDialog`, `PageHeader` استفاده کن — کتابخانه جدید اضافه نکن
|
||
- **RTL**: همه استایلها RTL باشند
|
||
- **کامیت بعد از هر تسک**: `git add` فایلهای تغییریافته → `git commit -m "feat: ..."`
|
||
|
||
---
|
||
|
||
## TASK-10 — مدیریت پرسنل (Staff)
|
||
|
||
### API
|
||
|
||
```
|
||
GET /api/v1/staff → لیست پرسنل فعال entity جاری
|
||
POST /api/v1/staff → ایجاد
|
||
PATCH /api/v1/staff/{uuid} → ویرایش
|
||
PATCH /api/v1/staff/{uuid}/toggle → فعال/غیرفعال
|
||
```
|
||
|
||
**Response GET:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"uuid": "...",
|
||
"full_name": "علی رضایی",
|
||
"phone": "09121234567",
|
||
"job_title": "پرستار",
|
||
"address": null,
|
||
"national_code": "0012345678",
|
||
"active": true,
|
||
"created_at": 1718000000
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**Response PATCH toggle:**
|
||
|
||
```json
|
||
{ "success": true, "data": { "active": false } }
|
||
```
|
||
|
||
### فایلهای جدید
|
||
|
||
**`assets/admin/pages/StaffPage.tsx`** — صفحه مدیریت پرسنل:
|
||
|
||
- `useQuery(['staff'])` → `GET /api/v1/staff` → `data?.data ?? []`
|
||
- جدول با ستونها: نام، سمت، تلفن، کد ملی، وضعیت (badge)، عملیات
|
||
- دکمه «افزودن پرسنل» → Modal ایجاد
|
||
- دکمه ویرایش (PencilIcon) → Modal ویرایش
|
||
- دکمه toggle (EyeIcon/EyeSlashIcon) با `ConfirmDialog` برای غیرفعالسازی
|
||
- حذف سخت نداریم — فقط toggle
|
||
|
||
**فیلدهای فرم:**
|
||
|
||
```ts
|
||
interface StaffFormData {
|
||
full_name: string; // required
|
||
phone?: string;
|
||
job_title?: string;
|
||
address?: string;
|
||
national_code?: string;
|
||
}
|
||
```
|
||
|
||
**Zod schema:**
|
||
|
||
```ts
|
||
const schema = z.object({
|
||
full_name: z.string().min(2, "نام حداقل ۲ کاراکتر باید باشد"),
|
||
phone: z.string().optional(),
|
||
job_title: z.string().optional(),
|
||
address: z.string().optional(),
|
||
national_code: z.string().optional(),
|
||
});
|
||
```
|
||
|
||
### Route و Sidebar
|
||
|
||
در `App.tsx` اضافه کن:
|
||
|
||
```tsx
|
||
<Route
|
||
path="staff"
|
||
element={
|
||
<RoleRoute roles={["doctor", "clinic"]}>
|
||
<StaffPage />
|
||
</RoleRoute>
|
||
}
|
||
/>
|
||
```
|
||
|
||
در Sidebar (فایل `components/layout/AdminLayout.tsx` یا `Sidebar.tsx`) لینک «پرسنل» را برای نقشهای `doctor` و `clinic` اضافه کن.
|
||
|
||
### کامیت
|
||
|
||
```
|
||
git add assets/admin/pages/StaffPage.tsx assets/admin/App.tsx assets/admin/components/layout/...
|
||
git commit -m "feat: add staff management page (TASK-10)"
|
||
```
|
||
|
||
---
|
||
|
||
## TASK-11 — پنل اشتراکی (Subscription)
|
||
|
||
### API
|
||
|
||
```
|
||
GET /api/v1/subscription/plans → لیست پلنها (public)
|
||
GET /api/v1/subscription/my → اشتراک فعال من
|
||
POST /api/v1/subscription/trial → فعالسازی تریال
|
||
POST /api/v1/subscription-payment → شروع پرداخت { period_uuid, gateway }
|
||
```
|
||
|
||
**Response GET /plans:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"uuid": "...",
|
||
"name": "basic",
|
||
"level": 1,
|
||
"max_secretaries": 2,
|
||
"features": { "patient_records": true, "services": true },
|
||
"periods": [
|
||
{
|
||
"uuid": "...",
|
||
"label": "تریال ۱ ماهه",
|
||
"duration_months": 1,
|
||
"price_rials": 0,
|
||
"is_trial": true
|
||
},
|
||
{
|
||
"uuid": "...",
|
||
"label": "۱ ماهه",
|
||
"duration_months": 1,
|
||
"price_rials": 250000,
|
||
"is_trial": false
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**Response GET /my:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"plan": {
|
||
"name": "basic",
|
||
"level": 1,
|
||
"features": { "patient_records": true, "services": true }
|
||
},
|
||
"period": { "label": "۶ ماهه", "duration_months": 6 },
|
||
"is_trial": false,
|
||
"starts_at": 1718000000,
|
||
"expires_at": 1733360000,
|
||
"used_trial": true,
|
||
"days_remaining": 42
|
||
}
|
||
}
|
||
```
|
||
|
||
### فایلهای جدید
|
||
|
||
**`assets/admin/pages/SubscriptionPage.tsx`** — صفحه اشتراک:
|
||
|
||
**بخش ۱ — وضعیت فعلی:**
|
||
|
||
- نمایش پلن فعلی (نام، تاریخ انقضا به شمسی، روزهای باقیمانده)
|
||
- اگر `used_trial = false` → دکمه «فعالسازی تریال رایگان» (Basic یک ماهه)
|
||
- نوار پیشرفت روزهای باقیمانده (اگر `expires_at` دارد)
|
||
|
||
**بخش ۲ — کارتهای پلن:**
|
||
|
||
- سه کارت: Free / Basic / Professional
|
||
- هر کارت: نام پلن، ویژگیها (لیست چکمارک)، دورههای قیمت به صورت badge/tab
|
||
- دکمه «خرید» برای هر دوره → modal انتخاب gateway → `POST /api/v1/subscription-payment` → redirect به URL برگشتی
|
||
- اگر پلن فعلی همان پلن است → دکمه «تمدید»
|
||
|
||
**Types جدید (`assets/admin/types/index.ts`):**
|
||
|
||
```ts
|
||
export interface SubscriptionPlan {
|
||
uuid: string;
|
||
name: string;
|
||
level: number;
|
||
max_secretaries: number;
|
||
features: Record<string, boolean>;
|
||
periods: SubscriptionPeriod[];
|
||
}
|
||
export interface SubscriptionPeriod {
|
||
uuid: string;
|
||
label: string;
|
||
duration_months: number;
|
||
price_rials: number;
|
||
is_trial: boolean;
|
||
}
|
||
export interface MySubscription {
|
||
plan: { name: string; level: number; features: Record<string, boolean> };
|
||
period?: { label: string; duration_months: number };
|
||
is_trial: boolean;
|
||
starts_at?: number;
|
||
expires_at?: number | null;
|
||
used_trial: boolean;
|
||
days_remaining?: number;
|
||
}
|
||
```
|
||
|
||
### Route
|
||
|
||
```tsx
|
||
<Route
|
||
path="subscription"
|
||
element={
|
||
<RoleRoute roles={["doctor", "clinic"]}>
|
||
<SubscriptionPage />
|
||
</RoleRoute>
|
||
}
|
||
/>
|
||
```
|
||
|
||
### کامیت
|
||
|
||
```
|
||
git commit -m "feat: add subscription management page (TASK-11)"
|
||
```
|
||
|
||
---
|
||
|
||
## TASK-12 — تکمیل Permissions منشی
|
||
|
||
**هیچ صفحه جدیدی لازم نیست** — فقط `assets/admin/pages/SecretariesPage.tsx` تغییر میکند.
|
||
|
||
### وضعیت فعلی SecretariesPage.tsx
|
||
|
||
فایل موجود است و لیست منشی ها را نشان میدهد. باید:
|
||
|
||
1. **Modal ویرایش permissions** اضافه کن:
|
||
- دکمه «دسترسیها» (ShieldCheckIcon) کنار هر ردیف
|
||
- Modal با checkbox matrix:
|
||
|
||
```tsx
|
||
// ساختار permissions که باید از API بخوانی و ذخیره کنی
|
||
const DEFAULT_PERMISSIONS = {
|
||
appointments: {
|
||
view: true,
|
||
create: false,
|
||
cancel: false,
|
||
update_status: false,
|
||
},
|
||
addresses: { view: true, create: false, update: false, delete: false },
|
||
clinic_info: { view: true, update: false },
|
||
insurances: { view: true, create: false, update: false, delete: false },
|
||
};
|
||
|
||
const PERMISSION_LABELS = {
|
||
appointments: {
|
||
label: "نوبتها",
|
||
actions: [
|
||
{ key: "view", label: "مشاهده" },
|
||
{ key: "create", label: "ایجاد" },
|
||
{ key: "cancel", label: "لغو" },
|
||
{ key: "update_status", label: "تغییر وضعیت" },
|
||
],
|
||
},
|
||
addresses: {
|
||
label: "آدرسها",
|
||
actions: [
|
||
{ key: "view", label: "مشاهده" },
|
||
{ key: "create", label: "ایجاد" },
|
||
{ key: "update", label: "ویرایش" },
|
||
{ key: "delete", label: "حذف" },
|
||
],
|
||
},
|
||
clinic_info: {
|
||
label: "اطلاعات کلینیک",
|
||
actions: [
|
||
{ key: "view", label: "مشاهده" },
|
||
{ key: "update", label: "ویرایش" },
|
||
],
|
||
},
|
||
insurances: {
|
||
label: "بیمهها",
|
||
actions: [
|
||
{ key: "view", label: "مشاهده" },
|
||
{ key: "create", label: "ایجاد" },
|
||
{ key: "update", label: "ویرایش" },
|
||
{ key: "delete", label: "حذف" },
|
||
],
|
||
},
|
||
};
|
||
```
|
||
|
||
2. **PATCH `/api/v1/secretary/{uuid}`** با body:
|
||
|
||
```json
|
||
{ "permissions": { "appointments": { "view": true, ... }, ... } }
|
||
```
|
||
|
||
3. **خطای `ERR_SECRETARY_LIMIT_REACHED` (403)** را هنگام ایجاد منشی handle کن:
|
||
|
||
```tsx
|
||
// در onError useMutation create:
|
||
if (err.code === "ERR_SECRETARY_LIMIT_REACHED") {
|
||
toast.error(
|
||
"سقف تعداد منشی پنل فعلی شما پر است. برای افزودن منشی بیشتر پنل را ارتقا دهید.",
|
||
);
|
||
}
|
||
```
|
||
|
||
### کامیت
|
||
|
||
```
|
||
git commit -m "feat: add secretary permissions matrix and subscription limit handling (TASK-12)"
|
||
```
|
||
|
||
---
|
||
|
||
## TASK-13 — سرویسهای کلینیک
|
||
|
||
### API
|
||
|
||
```
|
||
GET /api/v1/service-sections → لیست بخشها
|
||
POST /api/v1/service-section → ایجاد بخش { name }
|
||
PATCH /api/v1/service-section/{uuid} → ویرایش { name }
|
||
DELETE /api/v1/service-section/{uuid} → حذف بخش
|
||
|
||
GET /api/v1/service-items/{sectionUuid} → لیست آیتمهای یک بخش
|
||
POST /api/v1/service-item → ایجاد آیتم
|
||
PATCH /api/v1/service-item/{uuid} → ویرایش آیتم
|
||
DELETE /api/v1/service-item/{uuid} → حذف آیتم
|
||
```
|
||
|
||
**Response GET /service-sections:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [{ "uuid": "...", "name": "تزریقات", "active": true }]
|
||
}
|
||
```
|
||
|
||
**Response GET /service-items/{sectionUuid}:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"uuid": "...",
|
||
"name": "سرم ۵۰۰cc",
|
||
"price_rials": 85000,
|
||
"staff": { "uuid": "...", "full_name": "علی رضایی" },
|
||
"active": true
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### فایلهای جدید
|
||
|
||
**`assets/admin/pages/ClinicServicesPage.tsx`** — صفحه دوبخشی:
|
||
|
||
**ستون چپ** — لیست بخشها (ServiceSection):
|
||
|
||
- لیست ساده با دکمههای ویرایش/حذف
|
||
- دکمه «+ بخش جدید» → Modal با فیلد `name`
|
||
- کلیک روی بخش → بارگذاری آیتمهای آن در ستون راست
|
||
|
||
**ستون راست** — لیست آیتمهای بخش انتخابی (ServiceItem):
|
||
|
||
- جدول: نام، قیمت (`formatRial()`), پرسنل مسئول، وضعیت
|
||
- دکمه «+ آیتم جدید»
|
||
- حذف: اگر `ERR_SERVICE_ITEM_IN_USE` (409) → toast.error('این سرویس در پرونده بیمار استفاده شده و قابل حذف نیست')
|
||
|
||
**فرم آیتم:**
|
||
|
||
```ts
|
||
interface ServiceItemForm {
|
||
section_uuid: string; // hidden — از state
|
||
name: string; // required
|
||
price_rials: number; // required
|
||
staff_uuid?: string; // SearchableSelect از GET /api/v1/staff
|
||
}
|
||
```
|
||
|
||
**Types جدید:**
|
||
|
||
```ts
|
||
export interface ServiceSection {
|
||
uuid: string;
|
||
name: string;
|
||
active: boolean;
|
||
}
|
||
export interface ServiceItem {
|
||
uuid: string;
|
||
name: string;
|
||
price_rials: number;
|
||
staff: { uuid: string; full_name: string } | null;
|
||
active: boolean;
|
||
}
|
||
```
|
||
|
||
### Route
|
||
|
||
```tsx
|
||
<Route
|
||
path="clinic-services"
|
||
element={
|
||
<RoleRoute roles={["doctor", "clinic"]}>
|
||
<ClinicServicesPage />
|
||
</RoleRoute>
|
||
}
|
||
/>
|
||
```
|
||
|
||
### کامیت
|
||
|
||
```
|
||
git commit -m "feat: add clinic services management page (TASK-13)"
|
||
```
|
||
|
||
---
|
||
|
||
## TASK-14 — پنل پیامکی
|
||
|
||
### API
|
||
|
||
```
|
||
GET /api/v1/sms/wallet/balance → موجودی
|
||
POST /api/v1/sms/wallet/charge → شارژ { gateway, amount_rials } → redirect URL
|
||
GET /api/v1/sms/wallet/logs → تاریخچه (paginated)
|
||
GET /api/v1/sms/settings → تنظیمات
|
||
PATCH /api/v1/sms/settings → ذخیره تنظیمات
|
||
```
|
||
|
||
**Response GET /sms/wallet/balance:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"balance_rials": 150000,
|
||
"sms_price_rials": 250,
|
||
"estimated_sms_count": 600
|
||
}
|
||
}
|
||
```
|
||
|
||
**Response GET /sms/wallet/logs:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"uuid": "...",
|
||
"type": "credit",
|
||
"amount_rials": 500000,
|
||
"description": "شارژ",
|
||
"created_at": 1718000000
|
||
},
|
||
{
|
||
"uuid": "...",
|
||
"type": "debit",
|
||
"amount_rials": 250,
|
||
"description": "ارسال پیامک یادآوری",
|
||
"created_at": 1718001000
|
||
}
|
||
],
|
||
"meta": { "totalRecords": 45, "totalPages": 5, "currentPage": 1 }
|
||
}
|
||
```
|
||
|
||
**Response GET /sms/settings:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"reminder_enabled": true,
|
||
"reminder_hours_before": 3,
|
||
"post_visit_enabled": false,
|
||
"post_visit_text": null
|
||
}
|
||
}
|
||
```
|
||
|
||
### فایلهای جدید
|
||
|
||
**`assets/admin/pages/SmsWalletPage.tsx`** — صفحه کیف پول پیامک:
|
||
|
||
**بخش ۱ — کارت موجودی:**
|
||
|
||
- نمایش `balance_rials` با `formatRial()`
|
||
- نمایش `estimated_sms_count` (تعداد پیامک قابل ارسال)
|
||
- دکمه «شارژ کیف پول» → Modal با فیلد مبلغ + انتخاب gateway → redirect به URL
|
||
|
||
**بخش ۲ — تنظیمات:**
|
||
|
||
- Toggle «یادآوری قبل از نوبت» + فیلد عددی «چند ساعت قبل»
|
||
- Toggle «پیامک بعد از ویزیت» + textarea متن پیامک
|
||
- دکمه ذخیره → `PATCH /api/v1/sms/settings`
|
||
|
||
**بخش ۳ — تاریخچه تراکنشها:**
|
||
|
||
- جدول: نوع (badge سبز credit / قرمز debit)، مبلغ، توضیحات، تاریخ
|
||
- Pagination
|
||
|
||
**Types جدید:**
|
||
|
||
```ts
|
||
export interface SmsWalletBalance {
|
||
balance_rials: number;
|
||
sms_price_rials: number;
|
||
estimated_sms_count: number;
|
||
}
|
||
export interface SmsWalletLog {
|
||
uuid: string;
|
||
type: "credit" | "debit";
|
||
amount_rials: number;
|
||
description: string;
|
||
created_at: number;
|
||
}
|
||
export interface SmsSettings {
|
||
reminder_enabled: boolean;
|
||
reminder_hours_before: number;
|
||
post_visit_enabled: boolean;
|
||
post_visit_text: string | null;
|
||
}
|
||
```
|
||
|
||
### Route
|
||
|
||
```tsx
|
||
<Route
|
||
path="sms-wallet"
|
||
element={
|
||
<RoleRoute roles={["doctor", "clinic"]}>
|
||
<SmsWalletPage />
|
||
</RoleRoute>
|
||
}
|
||
/>
|
||
```
|
||
|
||
### کامیت
|
||
|
||
```
|
||
git commit -m "feat: add SMS wallet and settings page (TASK-14)"
|
||
```
|
||
|
||
---
|
||
|
||
## TASK-15 — پرونده بیمار
|
||
|
||
### API
|
||
|
||
```
|
||
GET /api/v1/patients → لیست بیماران (paginated, ?search=)
|
||
POST /api/v1/patient → ایجاد پرونده { user_uuid }
|
||
GET /api/v1/patient/{uuid} → جزئیات پرونده
|
||
GET /api/v1/patient/{uuid}/sessions → لیست مراجعات (paginated)
|
||
POST /api/v1/patient/{uuid}/session → ثبت مراجعه جدید
|
||
PATCH /api/v1/session/{uuid} → ویرایش مراجعه
|
||
```
|
||
|
||
**Response GET /patients:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"uuid": "...",
|
||
"user": {
|
||
"uuid": "...",
|
||
"fullName": "علی محمدی",
|
||
"phone": "09121234567"
|
||
},
|
||
"created_at": 1710000000
|
||
}
|
||
],
|
||
"meta": { "totalRecords": 12, "totalPages": 2, "currentPage": 1 }
|
||
}
|
||
```
|
||
|
||
**Response GET /patient/{uuid}/sessions:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"uuid": "...",
|
||
"appointment_uuid": null,
|
||
"visit_price_rials": 500000,
|
||
"base_insurance_discount_percent": "30.00",
|
||
"supplementary_discount_percent": "0.00",
|
||
"services_total_rials": 130000,
|
||
"final_price_rials": 445000,
|
||
"payment_method": "cash",
|
||
"notes": "...",
|
||
"created_at": 1718000000,
|
||
"updated_at": 1718000000
|
||
}
|
||
],
|
||
"meta": { "totalRecords": 5, "totalPages": 1, "currentPage": 1 }
|
||
}
|
||
```
|
||
|
||
**Request POST /patient/{uuid}/session:**
|
||
|
||
```json
|
||
{
|
||
"visit_price_rials": 500000,
|
||
"base_insurance_discount_percent": 30,
|
||
"supplementary_discount_percent": 0,
|
||
"payment_method": "cash",
|
||
"notes": "...",
|
||
"services": [{ "service_item_uuid": "...", "staff_uuid": null }]
|
||
}
|
||
```
|
||
|
||
### فایلهای جدید/تغییر
|
||
|
||
**`assets/admin/pages/MyPatientsPage.tsx`** — بازنویسی کامل:
|
||
|
||
**نمای لیست بیماران:**
|
||
|
||
- جستجو (`?search=`) با debounce ۴۰۰ms
|
||
- جدول: نام بیمار، تلفن، تاریخ ثبت، عملیات
|
||
- دکمه «+ بیمار جدید» → Modal با `SearchableSelect` برای پیدا کردن کاربر (search by phone)
|
||
- کلیک روی ردیف → نمای جزئیات پرونده (state محلی یا route `/my-patients/:uuid`)
|
||
|
||
**نمای جزئیات پرونده:**
|
||
|
||
- مشخصات بیمار + دکمه «+ مراجعه جدید»
|
||
- لیست مراجعات (accordion یا جدول):
|
||
- قیمت ویزیت، تخفیف بیمه پایه، تخفیف تکمیلی، مجموع سرویسها، قیمت نهایی
|
||
- وضعیت پرداخت (`payment_method`)
|
||
- یادداشت
|
||
- دکمه ویرایش (فقط `notes` و `payment_method`)
|
||
|
||
**Modal ثبت مراجعه جدید:**
|
||
|
||
```ts
|
||
interface SessionFormData {
|
||
visit_price_rials: number;
|
||
base_insurance_discount_percent: number; // 0-100
|
||
supplementary_discount_percent: number; // 0-100
|
||
payment_method: "cash" | "card" | "insurance" | "online" | "pending";
|
||
notes?: string;
|
||
services: { service_item_uuid: string; staff_uuid?: string }[];
|
||
}
|
||
```
|
||
|
||
- محاسبه `final_price_rials` به صورت live در فرانت:
|
||
|
||
```ts
|
||
const afterBase = visitPrice * (1 - baseDiscount / 100);
|
||
const afterSupp = afterBase * (1 - suppDiscount / 100);
|
||
const servicesTotal = services.reduce(
|
||
(sum, s) => sum + (s.price_rials || 0),
|
||
0,
|
||
);
|
||
const finalPrice = Math.round(afterSupp) + servicesTotal;
|
||
```
|
||
|
||
- افزودن سرویس: `SearchableSelect` از `GET /api/v1/service-items/{sectionUuid}` (باید ابتدا بخش را انتخاب کند)
|
||
|
||
**Types جدید:**
|
||
|
||
```ts
|
||
export interface PatientRecord {
|
||
uuid: string;
|
||
user: { uuid: string; fullName: string; phone: string };
|
||
created_at: number;
|
||
}
|
||
export interface PatientSession {
|
||
uuid: string;
|
||
appointment_uuid: string | null;
|
||
visit_price_rials: number;
|
||
base_insurance_discount_percent: string;
|
||
supplementary_discount_percent: string;
|
||
services_total_rials: number;
|
||
final_price_rials: number;
|
||
payment_method: string;
|
||
notes: string | null;
|
||
created_at: number;
|
||
updated_at: number;
|
||
}
|
||
```
|
||
|
||
### Route
|
||
|
||
```tsx
|
||
// مسیر موجود کافی است:
|
||
<Route
|
||
path="my-patients"
|
||
element={
|
||
<RoleRoute roles={["doctor", "secretary", "clinic"]}>
|
||
<MyPatientsPage />
|
||
</RoleRoute>
|
||
}
|
||
/>
|
||
```
|
||
|
||
### کامیت
|
||
|
||
```
|
||
git commit -m "feat: implement patient records and sessions page (TASK-15)"
|
||
```
|
||
|
||
---
|
||
|
||
## TASK-16 — داشبورد هوشمند با فیلتر زمانی
|
||
|
||
### تغییر API
|
||
|
||
```
|
||
GET /api/v1/dashboard/clinic?from=UNIX&to=UNIX → فیلدهای جدید در stats
|
||
GET /api/v1/dashboard/doctor?from=UNIX&to=UNIX → فیلدهای جدید در stats
|
||
GET /api/v1/admin/dashboard/charts?from=UNIX&to=UNIX → سریهای زمانی
|
||
```
|
||
|
||
**فیلدهای جدید در Response clinic/doctor:**
|
||
|
||
```json
|
||
{
|
||
"stats": {
|
||
"...فیلدهای قبلی...",
|
||
"sms_wallet_balance": 150000,
|
||
"unique_patients_count": 45,
|
||
"revenue_period_rials": 12500000
|
||
},
|
||
"period": { "from": 1717200000, "to": 1719792000 }
|
||
}
|
||
```
|
||
|
||
**Response GET /admin/dashboard/charts?from=UNIX&to=UNIX:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"appointments_by_day": [
|
||
{ "date": "06/01", "count": 12 }
|
||
],
|
||
"revenue_by_day": [
|
||
{ "date": "06/01", "amount": 3500000 }
|
||
],
|
||
"subscription_sales_by_plan": [
|
||
{ "plan": "basic", "count": 15, "revenue": 3750000 }
|
||
],
|
||
"appointment_status": [...],
|
||
"top_specialties": [...]
|
||
}
|
||
}
|
||
```
|
||
|
||
### تغییر `assets/admin/pages/DashboardPage.tsx`
|
||
|
||
**۱. فیلتر زمانی — اضافه کن:**
|
||
|
||
```tsx
|
||
type Preset = "this_week" | "this_month" | "3_months" | "custom";
|
||
|
||
// تبدیل به Unix:
|
||
function getRange(preset: Preset): { from: number; to: number } {
|
||
const now = Math.floor(Date.now() / 1000);
|
||
switch (preset) {
|
||
case "this_week":
|
||
// شنبهی این هفته — سادهترین راه: now - روزهای گذشته از شنبه
|
||
const dayOfWeek = new Date().getDay(); // 0=Sun,6=Sat
|
||
const daysSinceSat = (dayOfWeek + 1) % 7;
|
||
return { from: now - daysSinceSat * 86400, to: now };
|
||
case "this_month":
|
||
const d = new Date();
|
||
d.setDate(1);
|
||
d.setHours(0, 0, 0, 0);
|
||
return { from: Math.floor(d.getTime() / 1000), to: now };
|
||
case "3_months":
|
||
return { from: now - 90 * 86400, to: now };
|
||
default:
|
||
return { from: now - 30 * 86400, to: now };
|
||
}
|
||
}
|
||
```
|
||
|
||
preset buttons:
|
||
|
||
```tsx
|
||
<div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
|
||
{(["this_week", "this_month", "3_months"] as Preset[]).map((p) => (
|
||
<button
|
||
key={p}
|
||
className={`btn sm ${preset === p ? "primary" : ""}`}
|
||
onClick={() => setPreset(p)}
|
||
>
|
||
{p === "this_week"
|
||
? "این هفته"
|
||
: p === "this_month"
|
||
? "این ماه"
|
||
: "۳ ماه"}
|
||
</button>
|
||
))}
|
||
</div>
|
||
```
|
||
|
||
**۲. فیلدهای جدید در کارتهای آمار:**
|
||
|
||
- کارت «موجودی پیامک»: `stats.sms_wallet_balance` → `formatRial()`
|
||
- کارت «بیماران یکتا»: `stats.unique_patients_count`
|
||
- کارت «درآمد دوره»: `stats.revenue_period_rials` → `formatRial()`
|
||
|
||
**۳. نمودارها (برای admin role):**
|
||
|
||
از **SVG inline** موجود در DashboardPage استفاده کن (کلاس `SvgLineChart` که در فایل موجود است):
|
||
|
||
```tsx
|
||
// appointments_by_day و revenue_by_day دادههای خطی دارند
|
||
// فقط آرایه عددی را استخراج کن:
|
||
const apptData = charts?.appointments_by_day?.map((d: any) => d.count) ?? [];
|
||
const revData = charts?.revenue_by_day?.map((d: any) => d.amount) ?? [];
|
||
|
||
<SvgLineChart data={apptData} color="#3b82f6" />
|
||
<SvgLineChart data={revData} color="#22c55e" />
|
||
```
|
||
|
||
**subscription_sales_by_plan** — جدول ساده:
|
||
|
||
```tsx
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>پلن</th>
|
||
<th>تعداد</th>
|
||
<th>درآمد</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{subData.map((row) => (
|
||
<tr key={row.plan}>
|
||
<td>{row.plan}</td>
|
||
<td>{row.count}</td>
|
||
<td>{formatRial(row.revenue)}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
```
|
||
|
||
**API call با from/to:**
|
||
|
||
```tsx
|
||
const [preset, setPreset] = useState<Preset>("this_month");
|
||
const range = getRange(preset);
|
||
|
||
// برای admin:
|
||
const { data: charts } = useQuery({
|
||
queryKey: ["dashboard-charts", range.from, range.to],
|
||
queryFn: () =>
|
||
api.get(
|
||
`/api/v1/admin/dashboard/charts?from=${range.from}&to=${range.to}`,
|
||
),
|
||
});
|
||
|
||
// برای doctor:
|
||
const { data: doctorDash } = useQuery({
|
||
queryKey: ["dashboard-doctor", range.from, range.to],
|
||
queryFn: () =>
|
||
api.get(`/api/v1/dashboard/doctor?from=${range.from}&to=${range.to}`),
|
||
enabled: primaryRole === "doctor",
|
||
});
|
||
```
|
||
|
||
### کامیت
|
||
|
||
```
|
||
git commit -m "feat: add date range filter and new stats to dashboard (TASK-16)"
|
||
```
|
||
|
||
---
|
||
|
||
## ترتیب اجرا و چکلیست
|
||
|
||
```
|
||
[ ] TASK-10: StaffPage.tsx → route → sidebar → yarn dev → commit
|
||
[ ] TASK-11: SubscriptionPage.tsx → route → sidebar → yarn dev → commit
|
||
[ ] TASK-12: SecretariesPage.tsx (permissions modal + limit error) → yarn dev → commit
|
||
[ ] TASK-13: ClinicServicesPage.tsx → route → sidebar → yarn dev → commit
|
||
[ ] TASK-14: SmsWalletPage.tsx → route → sidebar → yarn dev → commit
|
||
[ ] TASK-15: MyPatientsPage.tsx (بازنویسی) → yarn dev → commit
|
||
[ ] TASK-16: DashboardPage.tsx (date range + new stats + charts) → yarn dev → commit
|
||
```
|
||
|
||
## قوانین اجرا
|
||
|
||
1. **یک تسک در یک زمان** — هرگز دو تسک را همزمان شروع نکن
|
||
2. **بعد از هر تسک `ddev exec yarn dev`** — هر TypeScript error را همانجا رفع کن
|
||
3. **اگر API error داشت** — فایل `docs/api/` مربوطه را بخوان
|
||
4. **Types جدید** را در `assets/admin/types/index.ts` اضافه کن
|
||
5. **Sidebar** — برای هر صفحه جدید، لینک sidebar را هم اضافه کن (در `AdminLayout.tsx` یا `Sidebar.tsx`)
|
||
6. **کامیت** بعد از `yarn dev` بدون خطا — نه قبل از آن
|
||
|
||
## فایلهای مرجع مهم
|
||
|
||
| فایل | نقش |
|
||
| ------------------------------------------------- | ------------------------------------------------------------ |
|
||
| `assets/admin/lib/api.ts` | fetch wrapper — `api.get/post/patch/delete` |
|
||
| `assets/admin/lib/utils.ts` | `formatRial`, `formatDate`, `formatDateTime`, `formatNumber` |
|
||
| `assets/admin/types/index.ts` | همه TypeScript interfaces |
|
||
| `assets/admin/stores/authStore.ts` | `primaryRole`, `dbUuid`, `doctorUuid` |
|
||
| `assets/admin/components/ui/DataTable.tsx` | جدول — `Column<T>[]` |
|
||
| `assets/admin/components/ui/Modal.tsx` | Modal |
|
||
| `assets/admin/components/ui/SearchableSelect.tsx` | dropdown با جستجو |
|
||
| `docs/api/staff.md` | API staff |
|
||
| `docs/api/subscription.md` | API subscription |
|
||
| `docs/api/clinic-services.md` | API services |
|
||
| `docs/api/sms.md` | API SMS wallet |
|
||
| `docs/api/patient.md` | API patient records |
|
||
| `docs/api/dashboard.md` | API dashboard |
|