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,172 @@
# معماری — تسک ۱۰: مدیریت پرسنل (Staff)
## ساختار فایل‌ها
```
src/Staff/
├── Controller/
│ └── StaffController.php
├── Entity/
│ └── ClinicStaff.php
├── Repository/
│ └── ClinicStaffRepository.php
└── Service/
└── StaffService.php
```
## Entity: ClinicStaff
```php
<?php
namespace App\Staff\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: \App\Staff\Repository\ClinicStaffRepository::class)]
#[ORM\Table(name: 'clinic_staff')]
#[ORM\Index(columns: ['entity_type', 'entity_id', 'active'], name: 'idx_clinic_staff_entity')]
class ClinicStaff
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 36, unique: true)]
private string $uuid;
// polymorphic — 'doctor' یا 'clinic'
#[ORM\Column(type: 'string', length: 10)]
private string $entityType;
#[ORM\Column(type: 'integer')]
private int $entityId;
#[ORM\Column(type: 'string', length: 200)]
private string $fullName;
#[ORM\Column(type: 'string', length: 20, nullable: true)]
private ?string $phone = null;
#[ORM\Column(type: 'string', length: 100, nullable: true)]
private ?string $jobTitle = null;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $address = null;
#[ORM\Column(type: 'string', length: 10, nullable: true)]
private ?string $nationalCode = null;
#[ORM\Column(type: 'boolean')]
private bool $active = true;
#[ORM\Column(type: 'integer')]
private int $createdAt;
#[ORM\Column(type: 'integer')]
private int $updatedAt;
public function __construct()
{
$this->uuid = (string) Uuid::v4();
$this->createdAt = time();
$this->updatedAt = time();
}
// getters/setters ...
}
```
## Controller: StaffController
```php
<?php
namespace App\Staff\Controller;
use App\Shared\Controller\BaseController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/api/v1')]
class StaffController extends BaseController
{
#[Route('/staff', methods: ['GET'])]
public function list(): JsonResponse
{
// 1. از JWT: entity_type و entity_id (db_key + db_uuid) را بگیر
// 2. StaffRepository::findByEntity($entityType, $entityId) صدا بزن
// 3. return $this->success($items);
}
#[Route('/staff', methods: ['POST'])]
public function create(Request $request): JsonResponse
{
// 1. validate: full_name اجباری
// 2. ClinicStaff جدید با entityType/entityId از JWT بساز
// 3. persist و flush
// 4. return $this->success($staff->toArray());
}
#[Route('/staff/{uuid}', methods: ['PATCH'])]
public function update(string $uuid, Request $request): JsonResponse
{
// 1. find by uuid → 404 اگر نبود
// 2. بررسی مالکیت: staff->entityType/entityId == JWT claim → 403 اگر نبود
// 3. ویرایش فیلدهای موجود در body
// 4. return $this->success($staff->toArray());
}
#[Route('/staff/{uuid}/toggle', methods: ['PATCH'])]
public function toggle(string $uuid): JsonResponse
{
// 1. find → ownership check
// 2. active = !active
// 3. return $this->success(['active' => $staff->isActive()]);
}
}
```
## Service: StaffService
```php
<?php
namespace App\Staff\Service;
class StaffService
{
public function toArray(ClinicStaff $staff): array
{
return [
'uuid' => $staff->getUuid(),
'full_name' => $staff->getFullName(),
'phone' => $staff->getPhone(),
'job_title' => $staff->getJobTitle(),
'address' => $staff->getAddress(),
'national_code' => $staff->getNationalCode(),
'active' => $staff->isActive(),
'created_at' => $staff->getCreatedAt(),
];
}
}
```
## نحوه خواندن entity_type / entity_id از JWT
```php
// در BaseController یا trait:
// JWT payload شامل: db_key ('doctor'/'clinic') و db_uuid (uuid entity)
// باید uuid را به id تبدیل کنیم:
$entityType = $this->getUser()->getDbKey(); // 'doctor' یا 'clinic'
$entityUuid = $this->getUser()->getDbUuid();
// سپس از Repository مربوطه id واقعی را بگیریم
```
## وابستگی‌های آینده
- `src/ClinicService/Entity/ServiceItem.php``staff_id FK → clinic_staff.id`
- `src/Patient/Entity/SessionService.php``staff_id FK → clinic_staff.id`
@@ -0,0 +1,51 @@
# پایگاه داده — تسک ۱۰: مدیریت پرسنل (Staff)
## جدول: clinic_staff
| ستون | نوع | توضیح |
|------|-----|-------|
| id | INT UNSIGNED AUTO_INCREMENT PK | |
| uuid | CHAR(36) UNIQUE NOT NULL | |
| entity_type | VARCHAR(10) NOT NULL | `'doctor'` یا `'clinic'` |
| entity_id | INT NOT NULL | FK به doctors.id یا clinics.id (bسته به entity_type) |
| full_name | VARCHAR(200) NOT NULL | نام و نام‌خانوادگی — اجباری |
| phone | VARCHAR(20) NULL | |
| job_title | VARCHAR(100) NULL | عنوان شغل |
| address | TEXT NULL | |
| national_code | CHAR(10) NULL | |
| active | TINYINT(1) NOT NULL DEFAULT 1 | غیرفعال = soft delete |
| created_at | INT NOT NULL | Unix timestamp |
| updated_at | INT NOT NULL | Unix timestamp |
## ایندکس‌ها
```sql
CREATE INDEX idx_clinic_staff_entity
ON clinic_staff(entity_type, entity_id, active);
```
## نکات مهم
- **هیچ FK خارجی** برای entity_id تعریف نمی‌شود — چون entity_type polymorphic است و می‌تواند doctor یا clinic باشد. مسئولیت یکپارچگی با لایه application است.
- `active = 0` به معنای حذف منطقی است — ردیف هرگز از جدول پاک نمی‌شود
- وقتی staff غیرفعال می‌شود، service_items و session_services قبلی دست‌نخورده می‌مانند
## Migration نمونه
```sql
CREATE TABLE clinic_staff (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) NOT NULL UNIQUE,
entity_type VARCHAR(10) NOT NULL,
entity_id INT NOT NULL,
full_name VARCHAR(200) NOT NULL,
phone VARCHAR(20) NULL,
job_title VARCHAR(100) NULL,
address TEXT NULL,
national_code CHAR(10) NULL,
active TINYINT(1) NOT NULL DEFAULT 1,
created_at INT NOT NULL,
updated_at INT NOT NULL,
INDEX idx_clinic_staff_entity (entity_type, entity_id, active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
+107
View File
@@ -0,0 +1,107 @@
# تسک ۱۰: مدیریت پرسنل (Staff)
## توضیح
پیاده‌سازی سیستم مدیریت پرسنل کلینیک/مطب. پرسنل حساب کاربری ندارند و نیازی به لاگین ندارند.
این تسک **پیش‌نیاز مستقیم** تسک ۱۳ (سرویس‌ها) و تسک ۱۵ (پرونده بیمار) است.
## Endpoint ها
| متد | مسیر | توضیح | نیاز به Auth |
|-----|------|-------|-------------|
| GET | `/api/v1/staff` | لیست پرسنل context جاری | بله (Doctor/Clinic) |
| POST | `/api/v1/staff` | ایجاد پرسنل جدید | بله |
| PATCH | `/api/v1/staff/{uuid}` | ویرایش اطلاعات | بله (مالک) |
| PATCH | `/api/v1/staff/{uuid}/toggle` | فعال/غیرفعال (soft delete) | بله (مالک) |
**مهم:** حذف سخت (`DELETE`) ممنوع است — تاریخچه سرویس‌ها و سشن‌های بیمار به پرسنل ارجاع دارند.
## پیش‌نیازها
- تسک ۰۲ (Auth — JWT + switch-context)
- تسک ۰۵ (Doctor entity)
- تسک ۰۶ (Clinic entity)
## زمان تخمینی
۶ تا ۸ ساعت
## نمونه Request
### POST /api/v1/staff
```json
{
"full_name": "علی رضایی",
"phone": "09121234567",
"job_title": "پرستار",
"address": "تهران، خیابان ولیعصر",
"national_code": "0012345678"
}
```
### PATCH /api/v1/staff/{uuid}
```json
{
"full_name": "علی رضایی",
"job_title": "سرپرستار",
"phone": "09129999999"
}
```
### PATCH /api/v1/staff/{uuid}/toggle
```json
{}
```
(body خالی — فقط وضعیت active را toggle می‌کند)
## نمونه Response
### GET /api/v1/staff
```json
{
"success": true,
"data": [
{
"uuid": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"full_name": "علی رضایی",
"phone": "09121234567",
"job_title": "پرستار",
"address": "تهران، خیابان ولیعصر",
"national_code": "0012345678",
"active": true,
"created_at": 1718000000
}
]
}
```
### POST /api/v1/staff (موفق)
```json
{
"success": true,
"data": {
"uuid": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"full_name": "علی رضایی",
"phone": "09121234567",
"job_title": "پرستار",
"address": null,
"national_code": "0012345678",
"active": true,
"created_at": 1718000000
}
}
```
### PATCH /api/v1/staff/{uuid}/toggle
```json
{
"success": true,
"data": { "active": false }
}
```
## کدهای خطا
| کد HTTP | توضیح |
|---------|-------|
| 401 | JWT معتبر نیست |
| 403 | کاربر مالک این پرسنل نیست |
| 404 | uuid پرسنل پیدا نشد |
| 422 | `full_name` خالی است |
@@ -0,0 +1,86 @@
# جریان کاربری — تسک ۱۰: مدیریت پرسنل (Staff)
## جریان ایجاد پرسنل جدید
```
مدیر کلینیک/دکتر وارد پنل می‌شود
GET /api/v1/staff
→ لیست پرسنل فعلی context جاری نمایش داده می‌شود
کاربر روی «افزودن پرسنل» کلیک می‌کند → Modal باز می‌شود
POST /api/v1/staff
{ full_name, phone?, job_title?, address?, national_code? }
├─► 422: full_name خالی است → نمایش خطا در فرم
└─► 201: پرسنل ایجاد شد → لیست refresh می‌شود
```
## جریان ویرایش پرسنل
```
کاربر روی آیکن ویرایش در جدول کلیک می‌کند
Modal با اطلاعات فعلی پر می‌شود
PATCH /api/v1/staff/{uuid}
{ full_name, phone, job_title, ... }
├─► 403: این پرسنل متعلق به شما نیست
└─► 200: ویرایش موفق → Modal بسته می‌شود
```
## جریان غیرفعال‌سازی (Soft Delete)
```
کاربر روی toggle در جدول کلیک می‌کند
PATCH /api/v1/staff/{uuid}/toggle
active: true → false یا false → true
در لیست: badge وضعیت تغییر می‌کند (فعال/غیرفعال)
```
## چرا حذف سخت ممنوع است
```
clinic_staff (active=false)
├── service_items.staff_id → هنوز به پرسنل ارجاع دارد
│ (تاریخچه سرویس‌ها حفظ می‌شود)
└── session_services.staff_id → سشن‌های قبلی بیمار
نام انجام‌دهنده را نشان می‌دهند
```
اگر پرسنل حذف سخت می‌شد:
- `service_items.staff_id` → NULL (انجام‌دهنده سرویس گم می‌شد)
- `session_services.staff_id` → NULL (تاریخچه ویزیت ناقص می‌شد)
## نمایش در Frontend (StaffPage.tsx)
```
┌─────────────────────────────────────────────────┐
│ پرسنل کلینیک [+ افزودن] │
├──────────┬──────────┬──────────┬────────────────┤
│ نام │ سمت │ تلفن │ وضعیت عملیات│
├──────────┼──────────┼──────────┼────────────────┤
│ علی رضایی│ پرستار │ 0912... │ ✅فعال ✏️ 🔘 │
│ مریم نوری│ منشی │ 0913... │ ❌غیرفعال✏️ 🔘│
└──────────┴──────────┴──────────┴────────────────┘
```
- ستون آخر: آیکن ویرایش (Modal) + toggle وضعیت
- غیرفعال‌ها با رنگ کمتر نمایش داده می‌شوند اما از لیست حذف نمی‌شوند
- فیلتر: «فقط فعال‌ها» / «همه» — query param `?active=1`