- Fix national code handling in staff creation and updates to support Persian digits. - Update ClinicStaff entity to allow longer national codes (up to 15 characters). - Implement support for clinic secretaries in SecretaryController, allowing creation without a doctor UUID. - Add a new endpoint to retrieve doctors associated with a clinic for secretary management. - Improve appointment management by ensuring doctors are selectable even when no appointments exist. - Extend PatientController to allow secretaries to create patient records if they have the appropriate permissions. - Introduce a PriceInput component for better price formatting in forms, supporting Persian digits. - Add a MockGateway for testing payment processes without real transactions. - Enhance SMS settings management with an approval flow for post-visit text messages, including new fields for pending text and status. - Update migrations to reflect changes in database schema for national codes and SMS settings.
448 lines
20 KiB
Markdown
448 lines
20 KiB
Markdown
# رفع باگها و افزودن قابلیتهای جدید — Phase 3
|
||
|
||
## زمینه
|
||
|
||
چندین باگ و قابلیت جدید شناسایی شدهاند که باید در یک session رفع/پیادهسازی شوند:
|
||
- باگهای backend روی staff/secretary/appointments
|
||
- قابلیتهای جدید: مدیریت درگاه پرداخت و SMS از پنل ادمین، post-visit text approval flow
|
||
|
||
---
|
||
|
||
## مشکل / هدف
|
||
|
||
### باگ ۱ — خطای 500 در ایجاد پرسنل (national_code > 10 chars)
|
||
|
||
`POST /api/v1/staff` با `national_code: "۴۲۳۲۰۵۱۰۹۸۷"` → HTTP 500
|
||
|
||
**ریشه:** `ClinicStaff.nationalCode` column طول ۱۰ دارد ولی کد ملی ۱۱ رقم فارسی ارسال شده. باید:
|
||
1. طول column به ۲۰ افزایش یابد (برای ارقام فارسی که ۲بایتی هستند)
|
||
2. یا ارقام فارسی به لاتین تبدیل شوند قبل از ذخیره
|
||
|
||
**راهحل:** ارقام فارسی را در controller به لاتین تبدیل کن + طول column را به ۱۵ افزایش بده.
|
||
|
||
```php
|
||
// در StaffController::create و update
|
||
$data['national_code'] = $this->toLatinDigits($data['national_code'] ?? null);
|
||
|
||
private function toLatinDigits(?string $str): ?string {
|
||
if ($str === null) return null;
|
||
return strtr($str, ['۰'=>'0','۱'=>'1','۲'=>'2','۳'=>'3','۴'=>'4','۵'=>'5','۶'=>'6','۷'=>'7','۸'=>'8','۹'=>'9',
|
||
'٠'=>'0','١'=>'1','٢'=>'2','٣'=>'3','٤'=>'4','٥'=>'5','٦'=>'6','٧'=>'7','٨'=>'8','٩'=>'9']);
|
||
}
|
||
```
|
||
|
||
### باگ ۲ — doctor_uuid: null در ایجاد منشی از پنل کلینیک
|
||
|
||
`POST /api/v1/secretary` با `{mobile_number: "09...", doctor_uuid: null}` → HTTP 422
|
||
|
||
**ریشه:** `MySecretariesPage.tsx` از `useAuthStore().doctorUuid` استفاده میکند. وقتی کاربر `clinic` است، `doctorUuid` در auth store null است چون clinic دارای uuid پزشک نیست.
|
||
|
||
**جریان فعلی:** `SecretaryController::create` فقط doctor_uuid میپذیرد و clinic نمیتواند منشی اضافه کند.
|
||
|
||
**راهحل — دو بخش:**
|
||
|
||
**الف — Backend:** `SecretaryController::create` باید clinic_uuid هم بپذیرد:
|
||
```php
|
||
// اگر doctor_uuid خالی بود و clinic_uuid داشتیم، منشی برای کلینیک ایجاد شود
|
||
$doctorUuid = trim($data['doctor_uuid'] ?? '');
|
||
$clinicUuid = trim($data['clinic_uuid'] ?? '');
|
||
|
||
if (!empty($clinicUuid) && empty($doctorUuid)) {
|
||
// منشی برای clinic — از ClinicRepository پیدا کن
|
||
$clinic = $this->clinicRepo->findByUuid($clinicUuid);
|
||
if ($clinic === null) return $this->error(...);
|
||
// clinic میتواند منشی داشته باشد — DoctorSecretary نیست، باید entity جدید یا متفاوت باشد
|
||
}
|
||
```
|
||
|
||
**توجه مهم:** `DoctorSecretary` entity مستقیماً به `Doctor` وصل است. برای clinic باید بررسی کنی:
|
||
- آیا `DoctorSecretary` دارای فیلد clinic FK هم هست؟
|
||
- اگر نه، باید تصمیم بگیری: یا کلینیک یکی از دکتران خود را انتخاب کند که منشی برای آن تعریف شود، یا یک entity جدید `ClinicSecretary` بسازیم.
|
||
|
||
**پیشنهاد سادهتر (frontend):** در `MySecretariesPage.tsx`، وقتی `primaryRole === 'clinic'`، یک dropdown نشان بده که کاربر ابتدا یکی از پزشکان کلینیک را انتخاب کند، سپس منشی برای آن پزشک تعریف شود. این تغییر فقط frontend است و API دست نمیخورد.
|
||
|
||
اگر clinic دکتری ندارد → پیام «ابتدا یک پزشک به کلینیک اضافه کنید».
|
||
|
||
```tsx
|
||
// در MySecretariesPage.tsx
|
||
const { dbUuid, primaryRole, doctorUuid } = useAuthStore();
|
||
|
||
// کلینیک باید پزشک انتخاب کند
|
||
const [selectedDoctorUuid, setSelectedDoctorUuid] = useState(
|
||
primaryRole === 'doctor' ? (doctorUuid ?? '') : ''
|
||
);
|
||
|
||
// query پزشکان کلینیک (فقط برای clinic)
|
||
const { data: clinicDoctorsData } = useQuery({
|
||
queryKey: ['clinic-doctors-for-secretary', dbUuid],
|
||
queryFn: () => api.get(`/api/v1/appointments/clinic-doctors/${dbUuid}`), // endpoint موجود را بررسی کن
|
||
enabled: primaryRole === 'clinic' && !!dbUuid,
|
||
});
|
||
```
|
||
|
||
**بررسی endpoint موجود برای لیست پزشکان کلینیک:** بررسی کن آیا endpointای برای لیست پزشکان یک کلینیک وجود دارد. اگر نه، باید در AdminApiController یا ClinicController اضافه شود.
|
||
|
||
### باگ ۳ — در AppointmentsPage پزشکی برای انتخاب نیست (پنل کلینیک)
|
||
|
||
**ریشه:** `doctors` از `appointments` محاسبه میشود (unique doctorها از نتایج query). اگر هنوز نوبتی ثبت نشده باشد، لیست پزشکان خالی است پس در `SearchableSelect` چیزی نمایش داده نمیشود.
|
||
|
||
**راهحل:** یک endpoint برای لیست پزشکان کلینیک بساز و در AppointmentsPage برای role=clinic استفاده کن:
|
||
|
||
```tsx
|
||
// در AppointmentsPage.tsx
|
||
const { data: clinicDoctorsData } = useQuery<ApiResponse<{uuid: string; name: string}[]>>({
|
||
queryKey: ['clinic-doctors', dbUuid],
|
||
queryFn: () => api.get(`/api/v1/clinic/${dbUuid}/doctors`),
|
||
enabled: isClinic && !!dbUuid,
|
||
});
|
||
|
||
// merge با doctors از appointments
|
||
const allDoctors = React.useMemo(() => {
|
||
const fromAppts = doctors; // موجود
|
||
const fromClinc = (clinicDoctorsData?.data ?? []).map(d => ({ uuid: d.uuid, name: d.name }));
|
||
const merged = new Map<string, string>();
|
||
[...fromClinc, ...fromAppts].forEach(d => merged.set(d.uuid, d.name));
|
||
return Array.from(merged.entries()).map(([uuid, name]) => ({ uuid, name }));
|
||
}, [doctors, clinicDoctorsData]);
|
||
```
|
||
|
||
**Backend endpoint جدید:** `GET /api/v1/clinic/{uuid}/doctors` — در `ClinicController` یا `AdminApiController` اضافه شود:
|
||
```php
|
||
#[Route('/api/v1/clinic/{uuid}/doctors', methods: ['GET'])]
|
||
public function clinicDoctors(string $uuid, #[CurrentUser] User $user): JsonResponse
|
||
{
|
||
$clinic = $this->clinicRepo->findByUuid($uuid);
|
||
// بررسی دسترسی: فقط صاحب clinic یا admin
|
||
// برگرداندن لیست پزشکان: uuid + نام
|
||
}
|
||
```
|
||
|
||
### باگ ۴ — منشی نمیتواند پرونده بیمار تشکیل دهد
|
||
|
||
**ریشه:** `PatientController::resolveEntity` فقط `ROLE_DOCTOR` و `ROLE_CLINIC` بررسی میکند. برای `ROLE_SECRETARY` عدد null برمیگرداند و `assertPatientGate` خطای 403 میدهد.
|
||
|
||
**راهحل:** `resolveEntity` را در PatientController گسترش بده تا secretary را پشتیبانی کند:
|
||
|
||
```php
|
||
// src/Patient/Controller/PatientController.php
|
||
// اضافه کردن SecretaryRepository به constructor
|
||
private readonly DoctorSecretaryRepository $secretaryRepo,
|
||
|
||
private function resolveEntity(User $user): array
|
||
{
|
||
if ($user->hasRole('ROLE_DOCTOR')) {
|
||
$doctor = $this->doctorRepo->findByUser($user);
|
||
return $doctor !== null ? ['doctor', $doctor->getId()] : ['doctor', null];
|
||
}
|
||
|
||
if ($user->hasRole('ROLE_CLINIC')) {
|
||
$clinic = $this->clinicRepo->findByUser($user);
|
||
return $clinic !== null ? ['clinic', $clinic->getId()] : ['clinic', null];
|
||
}
|
||
|
||
if ($user->hasRole('ROLE_SECRETARY')) {
|
||
// secretary دسترسی به entity پزشک خود را دارد اگر permissions.appointments.view = true باشد
|
||
$secretary = $this->secretaryRepo->findActiveByUser($user);
|
||
if ($secretary !== null && ($secretary->getPermissions()['appointments']['view'] ?? false)) {
|
||
return ['doctor', $secretary->getDoctor()->getId()];
|
||
}
|
||
}
|
||
|
||
return ['unknown', null];
|
||
}
|
||
```
|
||
|
||
**بررسی `findActiveByUser`:** آیا این متد در `DoctorSecretaryRepository` وجود دارد؟ اگر نه، اضافهاش کن:
|
||
```php
|
||
public function findActiveByUser(User $user): ?DoctorSecretary
|
||
{
|
||
return $this->findOneBy(['secretary' => $user, 'active' => true]);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### قابلیت ۵ — فرمت قیمت با کاما در فرمها
|
||
|
||
در فرمهای input قیمت (ClinicServicesPage، SmsWalletPage، SubscriptionPage)، وقتی کاربر عدد میزند باید با کاما نمایش داده شود، ولی مقدار واقعی (number) ذخیره شود.
|
||
|
||
**راهحل:** یک component `PriceInput` در `assets/admin/components/ui/` بساز:
|
||
|
||
```tsx
|
||
// assets/admin/components/ui/PriceInput.tsx
|
||
interface PriceInputProps {
|
||
value: number | '';
|
||
onChange: (value: number) => void;
|
||
placeholder?: string;
|
||
min?: number;
|
||
}
|
||
|
||
export default function PriceInput({ value, onChange, placeholder, min = 0 }: PriceInputProps) {
|
||
const [display, setDisplay] = useState(value ? new Intl.NumberFormat('fa-IR').format(Number(value)) : '');
|
||
|
||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
// حذف همه کاراکترهای غیر عددی (فارسی و لاتین)
|
||
const raw = e.target.value.replace(/[^\d۰-۹٠-٩]/g, '');
|
||
// تبدیل به لاتین
|
||
const latin = raw.replace(/[۰-۹]/g, d => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d)));
|
||
const num = parseInt(latin, 10) || 0;
|
||
onChange(num);
|
||
setDisplay(num ? new Intl.NumberFormat('fa-IR').format(num) : '');
|
||
};
|
||
|
||
return (
|
||
<input
|
||
type="text"
|
||
inputMode="numeric"
|
||
value={display}
|
||
onChange={handleChange}
|
||
placeholder={placeholder}
|
||
dir="ltr"
|
||
style={{ textAlign: 'left' }}
|
||
/>
|
||
);
|
||
}
|
||
```
|
||
|
||
استفاده در `ClinicServicesPage.tsx`:
|
||
```tsx
|
||
// جایگزین کردن
|
||
<input {...itemForm.register('price_rials')} type="number" .../>
|
||
// با
|
||
<PriceInput
|
||
value={itemForm.watch('price_rials') ?? 0}
|
||
onChange={(v) => itemForm.setValue('price_rials', v)}
|
||
min={0}
|
||
placeholder="85,000"
|
||
/>
|
||
```
|
||
|
||
---
|
||
|
||
### قابلیت ۶ — مدیریت درگاههای پرداخت از پنل ادمین
|
||
|
||
**Backend:**
|
||
1. کلیدهای پرداخت را در `SiteConfig` ذخیره کن (نه .env)
|
||
2. `SiteConfigController::ALLOWED_KEYS` را گسترش بده:
|
||
|
||
```php
|
||
private const ALLOWED_KEYS = [
|
||
// ... موجود ...
|
||
// درگاههای پرداخت
|
||
'mellat_terminal_id',
|
||
'mellat_username',
|
||
'mellat_password',
|
||
'sep_terminal_id',
|
||
'sep_enabled',
|
||
'mellat_enabled',
|
||
// درگاه SMS
|
||
'sms_provider', // kavenegar / mock
|
||
'kavenegar_api_key',
|
||
'kavenegar_sender',
|
||
'sms_price_rials',
|
||
// درگاه تست
|
||
'payment_test_mode', // true/false
|
||
];
|
||
```
|
||
|
||
3. `MellatGateway` و `SepGateway` را تغییر بده تا از `SiteConfigRepository` بخوانند به جای env:
|
||
|
||
```php
|
||
// MellatGateway.php
|
||
class MellatGateway implements PaymentGatewayInterface
|
||
{
|
||
public function __construct(
|
||
private readonly HttpClientInterface $httpClient,
|
||
private readonly SiteConfigRepository $configRepo,
|
||
) {}
|
||
|
||
private function getTerminalId(): string {
|
||
return $this->configRepo->get('mellat_terminal_id') ?? $_ENV['MELLAT_TERMINAL_ID'] ?? '';
|
||
}
|
||
// همانند برای username و password
|
||
```
|
||
|
||
4. یک **Mock Gateway** (درگاه تست) اضافه کن:
|
||
|
||
```php
|
||
// src/Payment/Gateway/MockGateway.php
|
||
class MockGateway implements PaymentGatewayInterface
|
||
{
|
||
public function getName(): string { return 'mock'; }
|
||
|
||
public function initiate(int $amountRials, string $orderId, string $callbackUrl): PaymentInitResult
|
||
{
|
||
// redirect به callback مستقیم با success
|
||
$successUrl = $callbackUrl . '&status=success&mock=1&order_id=' . $orderId;
|
||
return new PaymentInitResult(true, redirectUrl: $successUrl, token: 'mock-' . $orderId);
|
||
}
|
||
|
||
public function verify(array $callbackData): PaymentVerifyResult
|
||
{
|
||
return new PaymentVerifyResult(true, refId: 'MOCK-' . time());
|
||
}
|
||
}
|
||
```
|
||
|
||
5. در `PaymentController` (یا جایی که gateway انتخاب میشود)، وقتی `payment_test_mode = true` در config، از `MockGateway` استفاده کن.
|
||
|
||
**Frontend — صفحه جدید SettingsPage یا بخشی از SettingsPage:**
|
||
|
||
فعلاً `SettingsPage.tsx` در `assets/admin/pages/` وجود دارد. بخش «درگاه پرداخت» را به آن اضافه کن:
|
||
|
||
```tsx
|
||
// در SettingsPage.tsx بخش جدید اضافه کن
|
||
// یا یک صفحه جدید GatewaySettingsPage.tsx بساز
|
||
|
||
const PAYMENT_KEYS = ['mellat_enabled', 'mellat_terminal_id', 'mellat_username', 'mellat_password',
|
||
'sep_enabled', 'sep_terminal_id', 'payment_test_mode'];
|
||
const SMS_KEYS = ['sms_provider', 'kavenegar_api_key', 'kavenegar_sender', 'sms_price_rials'];
|
||
```
|
||
|
||
فیلدها:
|
||
- `payment_test_mode`: toggle — اگر true، درگاه mock استفاده شود
|
||
- `mellat_enabled` / `sep_enabled`: toggle
|
||
- `mellat_terminal_id`, `mellat_username`, `mellat_password`: text fields (password masked)
|
||
- `sep_terminal_id`: text field
|
||
- `sms_provider`: select (kavenegar / mock)
|
||
- `kavenegar_api_key`: text field
|
||
- `kavenegar_sender`: text field
|
||
- `sms_price_rials`: number
|
||
|
||
---
|
||
|
||
### قابلیت ۷ — approval flow برای post-visit SMS text
|
||
|
||
**وضعیت فعلی:** `SmsSettings.post_visit_text` یک فیلد ساده text است. وقتی doctor/clinic متن مینویسد، مستقیم ذخیره و استفاده میشود.
|
||
|
||
**هدف:** متن post-visit باید ابتدا توسط ادمین تأیید شود قبل از اینکه در ارسال SMS استفاده شود.
|
||
|
||
**Backend — تغییرات Entity:**
|
||
|
||
در `SmsSettings` سه فیلد جدید اضافه کن:
|
||
```php
|
||
#[ORM\Column(name: 'post_visit_text_pending', type: 'text', nullable: true)]
|
||
private ?string $postVisitTextPending = null; // متن در انتظار تأیید
|
||
|
||
#[ORM\Column(name: 'post_visit_text_status', type: 'string', length: 20)]
|
||
private string $postVisitTextStatus = 'none'; // none | pending | approved | rejected
|
||
|
||
#[ORM\Column(name: 'post_visit_text_reject_reason', type: 'text', nullable: true)]
|
||
private ?string $postVisitTextRejectReason = null;
|
||
```
|
||
|
||
منطق:
|
||
- وقتی doctor/clinic متن جدید ارسال میکند → `postVisitTextPending` ذخیره شود، `postVisitTextStatus = 'pending'`، `postVisitText` (approved) دست نخورد
|
||
- وقتی ادمین approve میکند → `postVisitText = postVisitTextPending`، `postVisitTextStatus = 'approved'`
|
||
- وقتی ادمین reject میکند → `postVisitTextStatus = 'rejected'`، `postVisitTextRejectReason` ذخیره شود
|
||
|
||
**Backend — endpoint ادمین:**
|
||
|
||
در `SmsWalletController` یا یک controller جدید اضافه کن:
|
||
```php
|
||
#[Route('/api/v1/admin/sms/settings/review', methods: ['GET'])]
|
||
#[IsGranted('ROLE_ADMIN')]
|
||
public function pendingReviews(Request $request): JsonResponse
|
||
{
|
||
// لیست همه sms_settings که post_visit_text_status = 'pending'
|
||
}
|
||
|
||
#[Route('/api/v1/admin/sms/settings/{id}/approve', methods: ['POST'])]
|
||
#[IsGranted('ROLE_ADMIN')]
|
||
public function approvePostVisit(int $id): JsonResponse
|
||
{
|
||
// $settings->setPostVisitText($settings->getPostVisitTextPending());
|
||
// $settings->setPostVisitTextStatus('approved');
|
||
}
|
||
|
||
#[Route('/api/v1/admin/sms/settings/{id}/reject', methods: ['POST'])]
|
||
#[IsGranted('ROLE_ADMIN')]
|
||
public function rejectPostVisit(int $id, Request $request): JsonResponse
|
||
{
|
||
// $settings->setPostVisitTextStatus('rejected');
|
||
// $settings->setPostVisitTextRejectReason($data['reason']);
|
||
}
|
||
```
|
||
|
||
**Backend — تغییر `updateSettings`:**
|
||
|
||
در `PATCH /api/v1/sms/settings`:
|
||
```php
|
||
if (array_key_exists('post_visit_text', $data)) {
|
||
// به جای ذخیره مستقیم، در pending بگذار
|
||
$settings->setPostVisitTextPending($data['post_visit_text']);
|
||
$settings->setPostVisitTextStatus('pending');
|
||
$settings->setPostVisitTextRejectReason(null);
|
||
// postVisitText (approved) دست نخورد
|
||
}
|
||
```
|
||
|
||
**Frontend — SmsWalletPage.tsx:**
|
||
|
||
نمایش وضعیت pending/approved/rejected:
|
||
```tsx
|
||
// بعد از textarea متن پیامک
|
||
{currentSettings.post_visit_text_status === 'pending' && (
|
||
<div style={{ background: 'oklch(0.98 0.04 60)', border: '1px solid oklch(0.85 0.1 60)', borderRadius: 8, padding: '8px 12px', fontSize: 12.5, color: 'oklch(0.5 0.15 60)' }}>
|
||
متن شما در انتظار تأیید مدیر است
|
||
</div>
|
||
)}
|
||
{currentSettings.post_visit_text_status === 'approved' && (
|
||
<div style={{ background: 'var(--primary-subtle)', ... }}>
|
||
✓ متن تأیید شده — در حال استفاده
|
||
</div>
|
||
)}
|
||
{currentSettings.post_visit_text_status === 'rejected' && (
|
||
<div style={{ background: '#fef2f2', ... }}>
|
||
رد شد: {currentSettings.post_visit_text_reject_reason}
|
||
</div>
|
||
)}
|
||
```
|
||
|
||
**Frontend — بخش ادمین در SmsPage.tsx یا پنل مدیریت:**
|
||
|
||
تب یا بخش جدید «تأیید متن پیامک» در `SmsPage.tsx` اضافه کن که لیست pending reviews را نشان دهد.
|
||
|
||
---
|
||
|
||
## فایلهای مرتبط
|
||
|
||
| فایل | نقش |
|
||
|------|-----|
|
||
| `src/Staff/Controller/StaffController.php` | باگ ۱: national_code validation |
|
||
| `src/Staff/Entity/ClinicStaff.php` | باگ ۱: column length |
|
||
| `migrations/` | باید migration جدید بسازی |
|
||
| `src/Secretary/Controller/SecretaryController.php` | باگ ۲: clinic secretary support |
|
||
| `src/Patient/Controller/PatientController.php` | باگ ۴: secretary resolveEntity |
|
||
| `src/Patient/Repository/PatientRecordRepository.php` | بررسی findActiveByUser |
|
||
| `src/Secretary/Repository/DoctorSecretaryRepository.php` | اضافه کردن findActiveByUser |
|
||
| `src/Clinic/Controller/ClinicController.php` | endpoint لیست پزشکان کلینیک |
|
||
| `src/Config/Controller/SiteConfigController.php` | قابلیت ۶: allowed keys گسترش |
|
||
| `src/Payment/Gateway/MellatGateway.php` | قابلیت ۶: از config بخواند |
|
||
| `src/Payment/Gateway/SepGateway.php` | قابلیت ۶: از config بخواند |
|
||
| `src/Payment/Gateway/MockGateway.php` | قابلیت ۶: درگاه تست (جدید) |
|
||
| `src/Sms/Entity/SmsSettings.php` | قابلیت ۷: فیلدهای approval |
|
||
| `src/Sms/Controller/SmsWalletController.php` | قابلیت ۷: updateSettings تغییر + endpoint ادمین |
|
||
| `assets/admin/pages/AppointmentsPage.tsx` | باگ ۳: clinic doctors query |
|
||
| `assets/admin/pages/MySecretariesPage.tsx` | باگ ۲: clinic با انتخاب پزشک |
|
||
| `assets/admin/pages/SmsWalletPage.tsx` | قابلیت ۷: نمایش وضعیت |
|
||
| `assets/admin/pages/SettingsPage.tsx` | قابلیت ۶: بخش درگاههای پرداخت |
|
||
| `assets/admin/pages/SmsPage.tsx` | قابلیت ۷: تأیید/رد متن (ادمین) |
|
||
| `assets/admin/components/ui/PriceInput.tsx` | قابلیت ۵: جدید |
|
||
| `assets/admin/lib/utils.ts` | قابلیت ۵: `toLatinDigits` helper |
|
||
|
||
---
|
||
|
||
## نکات مهم
|
||
|
||
1. **ترتیب اجرا:** ابتدا باگها (۱→۲→۳→۴)، بعد قابلیتها (۵→۶→۷)
|
||
2. **Migration لازم است** برای: `ClinicStaff.national_code` (length)، `SmsSettings` (سه فیلد جدید)
|
||
3. **`payment_test_mode`:** وقتی true است، gateway selector در SmsWalletPage باید `mock` را هم نشان بدهد
|
||
4. **Gateway از config:** اگر config خالی بود → fallback به env variable (backward compatible)
|
||
5. **`SiteConfigRepository::getAll()`:** مراقب باش کلیدهای حساس (password، api_key) را در response برنگردانی — یا mask کن (مثلاً `***` نشان بده) یا از GET response حذف کن
|
||
6. **approval flow:** فقط `postVisitText` (approved) در ارسال SMS واقعی استفاده شود؛ `postVisitTextPending` هرگز مستقیم ارسال نشود
|
||
7. **PatientController + secretary:** secretary فقط اگر `permissions.appointments.view = true` باشد میتواند پرونده ببیند — بررسی کن `patient_records` feature هم در subscription secretary لازم است یا نه
|
||
8. **`formatRial` موجود است** در `utils.ts` و از `Intl.NumberFormat('fa-IR')` استفاده میکند — `PriceInput` هم باید همین format را داشته باشد
|
||
9. **کلید `toLatinDigits`:** این helper را در `utils.ts` export کن تا در backend هم بتوانیم از آن الهام بگیریم (PHP version)
|
||
10. **docs/api باید update شود** برای هر endpoint جدید/تغییریافته: `docs/api/staff.md`، `docs/api/secretary.md`، `docs/api/admin.md`، `docs/api/sms.md`
|