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,130 @@
|
||||
# معماری — تسک ۱۳: سرویسهای کلینیک
|
||||
|
||||
## ساختار فایلها
|
||||
|
||||
```
|
||||
src/ClinicService/
|
||||
├── Controller/
|
||||
│ └── ClinicServiceController.php
|
||||
├── Entity/
|
||||
│ ├── ServiceSection.php
|
||||
│ └── ServiceItem.php
|
||||
└── Repository/
|
||||
├── ServiceSectionRepository.php
|
||||
└── ServiceItemRepository.php
|
||||
```
|
||||
|
||||
## Entity: ServiceSection
|
||||
|
||||
```php
|
||||
#[ORM\Entity(repositoryClass: ServiceSectionRepository::class)]
|
||||
#[ORM\Table(name: 'service_sections')]
|
||||
#[ORM\Index(columns: ['entity_type', 'entity_id'], name: 'idx_service_sections_entity')]
|
||||
class ServiceSection
|
||||
{
|
||||
#[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
||||
private string $uuid;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 10)]
|
||||
private string $entityType; // 'doctor' | 'clinic'
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $entityId;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 200)]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $active = true;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $createdAt;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $updatedAt;
|
||||
}
|
||||
```
|
||||
|
||||
## Entity: ServiceItem
|
||||
|
||||
```php
|
||||
#[ORM\Entity(repositoryClass: ServiceItemRepository::class)]
|
||||
#[ORM\Table(name: 'service_items')]
|
||||
class ServiceItem
|
||||
{
|
||||
#[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
||||
private string $uuid;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ServiceSection::class)]
|
||||
#[ORM\JoinColumn(name: 'section_id', nullable: false, onDelete: 'CASCADE')]
|
||||
private ServiceSection $section;
|
||||
|
||||
// staff nullable — پرسنل انجامدهنده
|
||||
#[ORM\ManyToOne(targetEntity: \App\Staff\Entity\ClinicStaff::class)]
|
||||
#[ORM\JoinColumn(name: 'staff_id', nullable: true, onDelete: 'SET NULL')]
|
||||
private ?\App\Staff\Entity\ClinicStaff $staff = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 200)]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $priceRials = 0;
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $active = true;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $createdAt;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $updatedAt;
|
||||
}
|
||||
```
|
||||
|
||||
## Controller: ClinicServiceController
|
||||
|
||||
```php
|
||||
#[Route('/api/v1')]
|
||||
class ClinicServiceController extends BaseController
|
||||
{
|
||||
// برای همه write actions → gate check اول:
|
||||
private function assertServicesFeature(): void
|
||||
{
|
||||
[$entityType, $entityId] = $this->resolveEntityContext();
|
||||
if (!$this->subscriptionService->hasFeature($entityType, $entityId, 'services')) {
|
||||
throw new AppException(ErrorCodes::ERR_SUBSCRIPTION_REQUIRED, null, 403);
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/service-sections', methods: ['GET'])]
|
||||
public function listSections(): JsonResponse { ... }
|
||||
|
||||
#[Route('/service-section', methods: ['POST'])]
|
||||
public function createSection(Request $request): JsonResponse
|
||||
{
|
||||
$this->assertServicesFeature();
|
||||
// ...
|
||||
}
|
||||
|
||||
// سایر endpoint ها ...
|
||||
}
|
||||
```
|
||||
|
||||
## نکته DELETE سرویس با استفاده در پرونده
|
||||
|
||||
```php
|
||||
// اگر session_services.service_item_id → این item باشد:
|
||||
// FK ON DELETE RESTRICT → Doctrine exception → catch و return 409
|
||||
try {
|
||||
$this->em->remove($item);
|
||||
$this->em->flush();
|
||||
} catch (\Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException $e) {
|
||||
return $this->error(ErrorCodes::ERR_SERVICE_ITEM_IN_USE, 'این سرویس در پرونده بیمار استفاده شده است', 409);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
# پایگاه داده — تسک ۱۳: سرویسهای کلینیک
|
||||
|
||||
## جدول: service_sections
|
||||
|
||||
| ستون | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| 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 | |
|
||||
| name | VARCHAR(200) NOT NULL | نام بخش |
|
||||
| active | TINYINT(1) NOT NULL DEFAULT 1 | |
|
||||
| created_at | INT NOT NULL | Unix timestamp |
|
||||
| updated_at | INT NOT NULL | Unix timestamp |
|
||||
|
||||
ایندکس:
|
||||
```sql
|
||||
INDEX idx_service_sections_entity ON service_sections(entity_type, entity_id)
|
||||
```
|
||||
|
||||
## جدول: service_items
|
||||
|
||||
| ستون | نوع | توضیح |
|
||||
|------|-----|-------|
|
||||
| id | INT UNSIGNED AUTO_INCREMENT PK | |
|
||||
| uuid | CHAR(36) UNIQUE NOT NULL | |
|
||||
| section_id | INT NOT NULL FK→service_sections.id ON DELETE CASCADE | |
|
||||
| staff_id | INT NULL FK→clinic_staff.id ON DELETE SET NULL | پرسنل انجامدهنده (اختیاری) |
|
||||
| name | VARCHAR(200) NOT NULL | نام سرویس |
|
||||
| price_rials | INT NOT NULL DEFAULT 0 | |
|
||||
| active | TINYINT(1) NOT NULL DEFAULT 1 | |
|
||||
| created_at | INT NOT NULL | |
|
||||
| updated_at | INT NOT NULL | |
|
||||
|
||||
## Migration نمونه
|
||||
|
||||
```sql
|
||||
CREATE TABLE service_sections (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid CHAR(36) NOT NULL UNIQUE,
|
||||
entity_type VARCHAR(10) NOT NULL,
|
||||
entity_id INT NOT NULL,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at INT NOT NULL,
|
||||
updated_at INT NOT NULL,
|
||||
INDEX idx_service_sections_entity (entity_type, entity_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE service_items (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid CHAR(36) NOT NULL UNIQUE,
|
||||
section_id INT NOT NULL,
|
||||
staff_id INT NULL,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
price_rials INT NOT NULL DEFAULT 0,
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at INT NOT NULL,
|
||||
updated_at INT NOT NULL,
|
||||
FOREIGN KEY (section_id) REFERENCES service_sections(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (staff_id) REFERENCES clinic_staff(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- `section_id ON DELETE CASCADE` — حذف بخش، همه زیربخشها را هم حذف میکند
|
||||
- `staff_id ON DELETE SET NULL` — غیرفعالسازی پرسنل، سرویس را حذف نمیکند فقط staff_id=NULL میشود
|
||||
- `session_services.service_item_id` در تسک ۱۵ با `ON DELETE RESTRICT` → اگر item در سشنی باشد، حذف block میشود
|
||||
@@ -0,0 +1,102 @@
|
||||
# تسک ۱۳: سرویسهای کلینیک (Clinic Services)
|
||||
|
||||
## توضیح
|
||||
پیادهسازی مدیریت سرویسهای کلینیک در دو سطح: بخش (ServiceSection) و زیربخش (ServiceItem).
|
||||
دسترسی فقط در پنل **Basic+** — gate: `hasFeature('services')`.
|
||||
در تسک ۱۵ (پرونده بیمار)، ServiceItems در سشنهای بیمار استفاده میشوند.
|
||||
|
||||
## Endpoint ها
|
||||
|
||||
| متد | مسیر | Gate | توضیح |
|
||||
|-----|------|------|-------|
|
||||
| GET | `/api/v1/service-sections` | Basic+ | لیست بخشها |
|
||||
| POST | `/api/v1/service-section` | Basic+ | ایجاد بخش |
|
||||
| PATCH | `/api/v1/service-section/{uuid}` | Basic+ | ویرایش بخش |
|
||||
| DELETE | `/api/v1/service-section/{uuid}` | Basic+ | حذف بخش |
|
||||
| GET | `/api/v1/service-items/{sectionUuid}` | Basic+ | لیست زیربخشهای یک بخش |
|
||||
| POST | `/api/v1/service-item` | Basic+ | ایجاد زیربخش |
|
||||
| PATCH | `/api/v1/service-item/{uuid}` | Basic+ | ویرایش زیربخش |
|
||||
| DELETE | `/api/v1/service-item/{uuid}` | Basic+ | حذف زیربخش |
|
||||
|
||||
**نکته DELETE:** حذف سخت مجاز است — اما اگر ServiceItem در session_services استفاده شده باشد، باید با خطا متوقف شود (FK ON DELETE RESTRICT).
|
||||
|
||||
## پیشنیازها
|
||||
- **تسک ۱۰** (ClinicStaff — برای staff_id در ServiceItem)
|
||||
- **تسک ۱۱** (Subscription — hasFeature gate)
|
||||
|
||||
## زمان تخمینی
|
||||
۸ تا ۱۰ ساعت
|
||||
|
||||
## نمونه Request
|
||||
|
||||
### POST /api/v1/service-section
|
||||
```json
|
||||
{
|
||||
"name": "تزریقات"
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/v1/service-item
|
||||
```json
|
||||
{
|
||||
"section_uuid": "uuid-of-section",
|
||||
"name": "سرم ۵۰۰cc",
|
||||
"price_rials": 85000,
|
||||
"staff_uuid": "uuid-of-staff-optional"
|
||||
}
|
||||
```
|
||||
|
||||
### PATCH /api/v1/service-item/{uuid}
|
||||
```json
|
||||
{
|
||||
"price_rials": 95000,
|
||||
"staff_uuid": null
|
||||
}
|
||||
```
|
||||
|
||||
## نمونه Response
|
||||
|
||||
### GET /api/v1/service-sections
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"uuid": "...",
|
||||
"name": "تزریقات",
|
||||
"active": true,
|
||||
"items_count": 3
|
||||
},
|
||||
{
|
||||
"uuid": "...",
|
||||
"name": "فیزیوتراپی",
|
||||
"active": true,
|
||||
"items_count": 7
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/v1/service-items/{sectionUuid}
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"uuid": "...",
|
||||
"name": "سرم ۵۰۰cc",
|
||||
"price_rials": 85000,
|
||||
"staff": { "uuid": "...", "full_name": "علی رضایی" },
|
||||
"active": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## کدهای خطا
|
||||
|
||||
| کد | HTTP | توضیح |
|
||||
|----|------|-------|
|
||||
| `ERR_SUBSCRIPTION_REQUIRED` | 403 | پنل Basic یا بالاتر لازم است |
|
||||
| `ERR_SERVICE_SECTION_HAS_ITEMS` | 409 | بخش دارای زیربخش است — ابتدا آنها را حذف کنید |
|
||||
| `ERR_SERVICE_ITEM_IN_USE` | 409 | سرویس در پرونده بیمار استفاده شده — قابل حذف نیست |
|
||||
@@ -0,0 +1,99 @@
|
||||
# جریان کاربری — تسک ۱۳: سرویسهای کلینیک
|
||||
|
||||
## جریان مشاهده و ایجاد سرویسها
|
||||
|
||||
```
|
||||
کاربر وارد صفحه «سرویسها» میشود
|
||||
│
|
||||
▼
|
||||
GET /api/v1/service-sections
|
||||
→ accordion دو سطحی نمایش داده میشود
|
||||
│
|
||||
بر روی هر بخش کلیک میکند:
|
||||
▼
|
||||
GET /api/v1/service-items/{sectionUuid}
|
||||
→ زیربخشها نمایش داده میشوند
|
||||
```
|
||||
|
||||
## ساختار نمایش (ServicesPage.tsx)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ سرویسهای کلینیک [+ بخش جدید] │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ ▼ تزریقات ✏️ 🗑️ │
|
||||
│ ├─ سرم ۵۰۰cc ۸۵,۰۰۰ ریال ✏️ 🗑️ │
|
||||
│ ├─ آمپول B12 ۴۵,۰۰۰ ریال ✏️ 🗑️ │
|
||||
│ └─ [+ سرویس جدید] │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ ► فیزیوتراپی ✏️ 🗑️ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## جریان ایجاد بخش جدید
|
||||
|
||||
```
|
||||
کاربر «+ بخش جدید» کلیک میکند → Modal باز میشود
|
||||
│
|
||||
▼
|
||||
POST /api/v1/service-section { name: 'تزریقات' }
|
||||
│
|
||||
├─► 403 ERR_SUBSCRIPTION_REQUIRED → «پنل Basic لازم است»
|
||||
└─► 201 → accordion بهروز میشود
|
||||
```
|
||||
|
||||
## جریان حذف بخش
|
||||
|
||||
```
|
||||
کاربر روی 🗑️ بخش کلیک میکند
|
||||
│
|
||||
▼
|
||||
ConfirmDialog: «آیا از حذف بخش "تزریقات" مطمئنید؟
|
||||
همه زیربخشهای آن هم حذف میشوند.»
|
||||
│
|
||||
▼
|
||||
DELETE /api/v1/service-section/{uuid}
|
||||
│
|
||||
├─► اگر هیچ زیربخشی ندارد → 200 حذف شد
|
||||
└─► اگر زیربخش دارد → 409 ERR_SERVICE_SECTION_HAS_ITEMS
|
||||
(یا اگر DELETE CASCADE تعریف شده: همه حذف میشوند)
|
||||
```
|
||||
|
||||
## جریان حذف زیربخش
|
||||
|
||||
```
|
||||
DELETE /api/v1/service-item/{uuid}
|
||||
│
|
||||
├─► اگر در هیچ session_services نیست → 200
|
||||
└─► اگر در session_services استفاده شده → 409 ERR_SERVICE_ITEM_IN_USE
|
||||
پیام: «این سرویس در پرونده بیمار ثبت شده و قابل حذف نیست.»
|
||||
```
|
||||
|
||||
## جریان انتخاب پرسنل انجامدهنده
|
||||
|
||||
```
|
||||
هنگام ایجاد/ویرایش ServiceItem:
|
||||
│
|
||||
▼
|
||||
GET /api/v1/staff → SearchableSelect از پرسنل فعال
|
||||
→ «علی رضایی (پرستار)»
|
||||
→ «مریم نوری (فیزیوتراپیست)»
|
||||
→ [بدون پرسنل مشخص]
|
||||
```
|
||||
|
||||
## gate check — کاربر Free
|
||||
|
||||
```
|
||||
کاربر Free روی «سرویسها» در sidebar کلیک میکند
|
||||
│
|
||||
▼
|
||||
ServicesPage.tsx → GET /api/v1/service-sections → 403
|
||||
│
|
||||
▼
|
||||
نمایش banner:
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 🔒 این قابلیت نیاز به پنل Basic یا بالاتر │
|
||||
│ دارد. │
|
||||
│ [مشاهده پنلها] │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
Reference in New Issue
Block a user