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:
@@ -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 };
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user