Implement SMS panel user flow and patient records system; add wallet charging, automatic reminders, and patient session management with detailed database schema and user flows.

This commit is contained in:
hamed
2026-06-14 21:15:40 +03:30
parent a72a6da621
commit b58aacc37f
28 changed files with 3169 additions and 0 deletions
@@ -0,0 +1,100 @@
# معماری — تسک ۱۶: داشبورد هوشمند
## فایل‌هایی که تغییر می‌کنند
```
src/Dashboard/Controller/DashboardController.php ← اضافه کردن from/to به query + فیلدهای جدید
src/Admin/Controller/AdminApiController.php ← متد dashboardCharts() → محدود به بازه زمانی
assets/admin/pages/DashboardPage.tsx ← date range selector + نمودارها
```
## تغییر Backend
### DashboardController — اضافه کردن from/to
```php
#[Route('/api/v1/dashboard/clinic', methods: ['GET'])]
public function clinic(Request $request): JsonResponse
{
$from = (int) $request->query->get('from', strtotime('-30 days'));
$to = (int) $request->query->get('to', time());
// query موجود را محدود به بازه می‌کنیم:
// WHERE created_at BETWEEN :from AND :to
// فیلدهای جدید:
$smsBalance = $this->smsWalletRepo->getBalance($entityType, $entityId);
$uniquePatients = $this->patientRecordRepo->countUnique($entityType, $entityId, $from, $to);
$revenue = $this->patientSessionRepo->sumRevenue($entityType, $entityId, $from, $to);
return $this->success([
// ... فیلدهای موجود ...
'sms_wallet_balance' => $smsBalance,
'unique_patients_count' => $uniquePatients,
'revenue_period_rials' => $revenue,
]);
}
```
### AdminApiController — dashboardCharts با بازه زمانی
```php
#[Route('/api/v1/admin/dashboard/charts', methods: ['GET'])]
public function dashboardCharts(Request $request): JsonResponse
{
$from = (int) $request->query->get('from', strtotime('-30 days'));
$to = (int) $request->query->get('to', time());
// appointments_by_day: GROUP BY DATE(FROM_UNIXTIME(created_at))
// revenue_by_day: از patient_sessions در بازه
// subscription_sales_by_plan: از clinic_subscriptions در بازه
}
```
## تغییر Frontend — DashboardPage.tsx
### date range selector component:
```tsx
type DateRangePreset = 'week' | 'month' | '3months' | 'custom';
interface DateRange {
from: number; // Unix timestamp
to: number;
}
function DateRangeSelector({ value, onChange }: {
value: DateRange;
onChange: (r: DateRange) => void;
}) {
// preset buttons + PersianCalendar برای custom range
}
```
### نمودارها با Recharts:
```tsx
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
// chart نوبت‌ها
<ResponsiveContainer width="100%" height={250}>
<LineChart data={chartsData?.appointments_by_day}>
<XAxis dataKey="date" tickFormatter={d => formatDate(d)} />
<YAxis />
<Tooltip />
<Line dataKey="count" stroke="#3b82f6" />
</LineChart>
</ResponsiveContainer>
// chart درآمد
<BarChart data={chartsData?.revenue_by_day}>
<Bar dataKey="amount_rials" fill="#10b981" />
</BarChart>
```
### نصب dependency:
```bash
ddev exec yarn add recharts
ddev exec yarn add @types/recharts # اگر نیاز بود
```
@@ -0,0 +1,63 @@
# پایگاه داده — تسک ۱۶: داشبورد هوشمند
## هیچ migration لازم نیست
همه داده‌ها از جداول موجود و جداول ساخته‌شده در تسک‌های قبل خوانده می‌شوند.
## Query های جدید
### نوبت‌ها بر اساس روز (admin chart)
```sql
SELECT
FLOOR(created_at / 86400) * 86400 AS date_unix,
COUNT(*) AS count
FROM appointments
WHERE created_at BETWEEN :from AND :to
GROUP BY date_unix
ORDER BY date_unix ASC
```
### درآمد بر اساس روز (از patient_sessions)
```sql
SELECT
FLOOR(created_at / 86400) * 86400 AS date_unix,
SUM(final_price_rials) AS amount_rials
FROM patient_sessions
WHERE entity_type = :entityType
AND record_id IN (
SELECT id FROM patient_records WHERE entity_type = :entityType AND entity_id = :entityId
)
AND created_at BETWEEN :from AND :to
GROUP BY date_unix
ORDER BY date_unix ASC
```
### فروش اشتراک بر اساس پنل (admin)
```sql
SELECT
sp.name AS plan,
COUNT(cs.id) AS count,
SUM(p.amount) AS total_rials
FROM clinic_subscriptions cs
JOIN subscription_plans sp ON cs.plan_id = sp.id
LEFT JOIN payments p ON cs.payment_id = p.id
WHERE cs.is_trial = 0
AND cs.created_at BETWEEN :from AND :to
GROUP BY sp.name
```
### بیماران منحصربه‌فرد در بازه
```sql
SELECT COUNT(DISTINCT pr.user_id) AS unique_count
FROM patient_records pr
JOIN patient_sessions ps ON ps.record_id = pr.id
WHERE pr.entity_type = :entityType
AND pr.entity_id = :entityId
AND ps.created_at BETWEEN :from AND :to
```
## نکات مهم
- تمام `created_at` ها Unix timestamp هستند — فیلتر بازه زمانی مستقیم روی عدد اعمال می‌شود
- گروه‌بندی روزانه: `FLOOR(created_at / 86400) * 86400` → شروع روز به Unix
- نمایش در frontend: تبدیل Unix به تاریخ شمسی با `formatDate()`
@@ -0,0 +1,78 @@
# تسک ۱۶: داشبورد هوشمند — چارت + فیلتر زمانی
## توضیح
داشبوردهای موجود را با نمودار و فیلتر بازه زمانی تکمیل می‌کند.
**هیچ endpoint جدیدی ایجاد نمی‌شود** — فقط پارامتر `from` و `to` (Unix timestamp) به endpoint های موجود اضافه می‌شود.
همچنین چند فیلد جدید به response های موجود اضافه می‌شود.
## Endpoint های موجود که تغییر می‌کنند
| متد | مسیر | تغییر |
|-----|------|-------|
| GET | `/api/v1/admin/dashboard/charts` | اضافه: query params `from` و `to` |
| GET | `/api/v1/dashboard/clinic` | اضافه: `from`، `to` + فیلدهای جدید response |
| GET | `/api/v1/dashboard/doctor` | اضافه: `from`، `to` + فیلدهای جدید response |
## فیلدهای جدید در Response
### GET /api/v1/dashboard/clinic
فیلدهای اضافه‌شده به data موجود:
```json
{
"sms_wallet_balance": 150000,
"unique_patients_count": 45,
"revenue_period_rials": 12500000
}
```
### GET /api/v1/dashboard/doctor
فیلدهای اضافه‌شده:
```json
{
"unique_patients_count": 28,
"revenue_period_rials": 7800000
}
```
### GET /api/v1/admin/dashboard/charts?from=UNIX&to=UNIX
```json
{
"success": true,
"data": {
"appointments_by_day": [
{ "date": 1718000000, "count": 12 },
{ "date": 1718086400, "count": 8 }
],
"revenue_by_day": [
{ "date": 1718000000, "amount_rials": 4500000 },
{ "date": 1718086400, "amount_rials": 3200000 }
],
"subscription_sales_by_plan": [
{ "plan": "basic", "count": 15, "total_rials": 3750000 },
{ "plan": "professional", "count": 5, "total_rials": 2500000 }
]
}
}
```
## پیش‌نیازها
- همه تسک‌های ۱۰–۱۵ (نیاز به داده واقعی)
- `yarn add recharts` برای نمودارها
## زمان تخمینی
۸ تا ۱۰ ساعت
## فیلتر بازه زمانی
| preset | محاسبه |
|--------|--------|
| این هفته | from = شروع هفته جاری (شنبه) — to = now |
| این ماه | from = اول ماه جاری (شمسی) — to = now |
| ۳ ماه | from = now - 90 روز — to = now |
| سفارشی | کاربر از PersianCalendar انتخاب می‌کند |
**تبدیل تاریخ شمسی به Unix timestamp** برای ارسال به API:
```ts
// از کتابخانه موجود در پروژه استفاده می‌شود
// PersianCalendar component از assets/admin/components/ui/
```
@@ -0,0 +1,98 @@
# جریان کاربری — تسک ۱۶: داشبورد هوشمند
## جریان انتخاب بازه زمانی
```
کاربر وارد صفحه داشبورد می‌شود
بازه پیش‌فرض: «این ماه»
from = اول ماه جاری (شمسی → Unix)
to = now
GET /api/v1/dashboard/clinic?from=UNIX&to=UNIX
کاربر روی preset کلیک می‌کند:
[این هفته] [این ماه] [۳ ماه] [سفارشی]
انتخاب «سفارشی»:
PersianCalendar range picker باز می‌شود
کاربر از/تا را انتخاب می‌کند
URL update: ?from=UNIX&to=UNIX
TanStack Query refetch می‌شود
```
## جریان نمایش داشبورد کلینیک/مطب
```
┌──────────────────────────────────────────────────────────────┐
│ داشبورد کلینیک │
│ [این هفته] [این ماه ★] [۳ ماه] [سفارشی] │
├──────────┬──────────┬──────────┬──────────────────────────── │
│ نوبت‌های │ بیماران │ درآمد │ موجودی پیامک │
│ امروز: ۸ │ منحصربه │ این ماه │ ۱۵۰,۰۰۰ ریال │
│ │ فرد: ۴۵ │ ۱۲.۵M │ │
├──────────┴──────────┴──────────┴──────────────────────────── │
│ نمودار نوبت‌ها — این ماه │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ▄ ▄ ▄ ▄ │ │
│ │ ▄ ▄ ▄ ▄ ▄ ▄ ▄ ▄ ▄ ▄ ▄ │ │
│ │ ────────────────────────────────────── روز │ │
│ └────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────── │
│ نمودار درآمد — این ماه │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ █ █ █ █ │ │
│ │ █ █ █ █ █ █ █ █ █ │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
## جریان داشبورد ادمین
```
GET /api/v1/admin/dashboard/stats → کارت‌های آمار کلی
GET /api/v1/admin/dashboard/charts?from=UNIX&to=UNIX → نمودارها
نمودار اضافه: «فروش اشتراک بر اساس پنل»
basic: ۱۵ فروش | professional: ۵ فروش
(Bar chart با رنگ‌بندی متفاوت برای هر plan)
```
## فیلتر نقش — منشی
```
منشی وارد داشبورد می‌شود
→ فقط آمار نوبت‌ها نمایش داده می‌شود
→ فیلد درآمد: مخفی
→ فیلد بیماران: مخفی (اگر پنل Basic+ نباشد)
→ موجودی پیامک: مخفی
```
## preset ها — محاسبه از/تا
```typescript
function getDateRange(preset: DateRangePreset): DateRange {
const now = Math.floor(Date.now() / 1000);
switch (preset) {
case 'week':
// شنبه این هفته
const dayOfWeek = new Date().getDay(); // 0=یکشنبه ... 6=شنبه
const daysToSaturday = dayOfWeek === 6 ? 0 : dayOfWeek + 1;
return { from: now - daysToSaturday * 86400, to: now };
case 'month':
// اول ماه جاری شمسی → Unix timestamp
// از PersianCalendar helper استفاده می‌شود
return { from: startOfCurrentPersianMonth(), to: now };
case '3months':
return { from: now - 90 * 86400, to: now };
}
}
```