# پیاده‌سازی فرانت‌اند فاز ۲ — 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 } /> ``` در 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; 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 }; period?: { label: string; duration_months: number }; is_trial: boolean; starts_at?: number; expires_at?: number | null; used_trial: boolean; days_remaining?: number; } ``` ### Route ```tsx } /> ``` ### کامیت ``` 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 } /> ``` ### کامیت ``` 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 } /> ``` ### کامیت ``` 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 // مسیر موجود کافی است: } /> ``` ### کامیت ``` 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
{(["this_week", "this_month", "3_months"] as Preset[]).map((p) => ( ))}
``` **۲. فیلدهای جدید در کارت‌های آمار:** - کارت «موجودی پیامک»: `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) ?? []; ``` **subscription_sales_by_plan** — جدول ساده: ```tsx {subData.map((row) => ( ))}
پلن تعداد درآمد
{row.plan} {row.count} {formatRial(row.revenue)}
``` **API call با from/to:** ```tsx const [preset, setPreset] = useState("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[]` | | `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 |