140 lines
9.5 KiB
Markdown
140 lines
9.5 KiB
Markdown
# قیمت هر پیامک در تنظیمات + رفع تنظیمات ارسال پیامک کیفپول
|
|
|
|
## پروژه
|
|
|
|
`clinicpro` (Backend + Admin SPA).
|
|
|
|
## زمینه
|
|
|
|
دو مشکل در بخش پیامک پنل ادمین:
|
|
|
|
1. **`/admin/settings` → بخش «پیامک»**: امکان تعیین «هزینه هر پیامک» وجود ندارد (قبلاً بود). قیمت هر پیامک اکنون در بکاند **هاردکد** است: `SmsWalletController::SMS_PRICE_RIALS = 500`. باید به یک تنظیمِ قابلویرایش در همین صفحه تبدیل شود.
|
|
2. **`/admin/sms-wallet` → «تنظیمات ارسال پیامک»**: درست کار نمیکند — بعد از ذخیره، وضعیت واقعی سرور (بهویژه وضعیت تأیید متن پیامک بعد از ویزیت) در UI منعکس نمیشود، چون state محلی بعد از ذخیره/رفچ ریست نمیشود.
|
|
|
|
## مشکل / هدف
|
|
|
|
- افزودن کلید تنظیم `sms_price_rials` (قیمت هر پیامک) که در `/admin/settings` بخش پیامک قابل ویرایش باشد و بکاند بهجای مقدار هاردکد از آن استفاده کند.
|
|
- رفع باگِ state کهنه در فرم تنظیمات ارسالِ `/admin/sms-wallet`.
|
|
|
|
## فایلهای مرتبط
|
|
|
|
| فایل | نقش |
|
|
|------|-----|
|
|
| `src/Config/Controller/SiteConfigController.php` | `ALLOWED_KEYS` تنظیمات سایت (باید `sms_price_rials` اضافه شود) |
|
|
| `src/Sms/Controller/SmsWalletController.php` | استفاده از قیمت پیامک (هاردکد) + `resolveEntity` + endpoint تنظیمات |
|
|
| `assets/admin/pages/SettingsPage.tsx` | فرم تنظیمات؛ بخش `sms` |
|
|
| `assets/admin/pages/SmsWalletPage.tsx` | فرم «تنظیمات ارسال پیامک» (state محلی) |
|
|
| `docs/api/sms.md` | مستندات (بهروزرسانی) |
|
|
|
|
## وضعیت فعلی
|
|
|
|
قیمت هاردکد و balance:
|
|
|
|
```php
|
|
// src/Sms/Controller/SmsWalletController.php
|
|
private const SMS_PRICE_RIALS = 500;
|
|
// ...
|
|
$smsPriceRials = self::SMS_PRICE_RIALS;
|
|
$estimatedSms = (int) floor($balanceRials / $smsPriceRials);
|
|
return $this->success([
|
|
'balance_rials' => $balanceRials,
|
|
'sms_price_rials' => $smsPriceRials,
|
|
'estimated_sms_count' => $estimatedSms,
|
|
]);
|
|
```
|
|
|
|
کلیدهای مجاز تنظیمات (بدون `sms_price_rials`):
|
|
|
|
```php
|
|
// src/Config/Controller/SiteConfigController.php
|
|
private const ALLOWED_KEYS = [
|
|
// ...
|
|
'sms_panel_fee_rials',
|
|
'appointment_fee_rials',
|
|
// ...
|
|
];
|
|
```
|
|
|
|
بخش پیامکِ `SettingsPage.tsx` فقط وضعیت کلید API را نشان میدهد (فیلد قیمت ندارد):
|
|
|
|
```tsx
|
|
{current.id === 'sms' && (
|
|
// فقط sms_api_key_configured نمایش داده میشود
|
|
)}
|
|
```
|
|
|
|
فرم تنظیماتِ `SmsWalletPage.tsx` — state محلی بعد از ذخیره ریست نمیشود:
|
|
|
|
```tsx
|
|
const [localSettings, setLocalSettings] = useState<SmsSettings | null>(null);
|
|
const currentSettings = localSettings ?? settings;
|
|
|
|
const saveMutation = useMutation({
|
|
mutationFn: (body: SmsSettings) => api.patch('/api/v1/sms/settings', body),
|
|
onSuccess: () => { qc.invalidateQueries({ queryKey: ['sms-settings'] }); toast.success('تنظیمات ذخیره شد'); },
|
|
// ❌ localSettings ریست نمیشود → currentSettings همان نسخهٔ ویرایششده میماند،
|
|
// دادهی تازهٔ سرور (مثل post_visit_text_status = 'pending') نمایش داده نمیشود
|
|
});
|
|
```
|
|
|
|
## وظایف
|
|
|
|
### ۱. افزودن کلید `sms_price_rials` به تنظیمات سایت (Backend)
|
|
|
|
در `SiteConfigController::ALLOWED_KEYS` کلید `'sms_price_rials'` را اضافه کن (کنار `sms_panel_fee_rials`). با این کار GET/PATCH `/api/v1/admin/settings` این کلید را برمیگرداند/ذخیره میکند.
|
|
|
|
### ۲. استفادهٔ بکاند از قیمتِ قابلتنظیم بهجای هاردکد
|
|
|
|
در `SmsWalletController`:
|
|
- `SiteConfigRepository` را به constructor تزریق کن (اگر نیست).
|
|
- یک helper خصوصی بساز: `private function smsPriceRials(): int { return max(1, (int) ($this->configRepo->get('sms_price_rials') ?: self::SMS_PRICE_RIALS)); }` و `SMS_PRICE_RIALS = 500` را بهعنوان **fallback** نگهدار.
|
|
- در `balance()` بهجای `self::SMS_PRICE_RIALS` از `$this->smsPriceRials()` استفاده کن.
|
|
- **جستجو کن** آیا جای دیگری قیمت هر پیامک برای کسر از کیفپول هنگام ارسال استفاده میشود (مثلاً `SmsWalletService` یا مسیر ارسال پیامک). اگر بله، همانجا هم از `sms_price_rials` (config) استفاده شود تا کسر و «تعداد تخمینی» همخوان باشند. اگر جایی مقدار ثابت دیگری هست، آن را هم به config متصل کن.
|
|
|
|
### ۳. فیلد «هزینه هر پیامک» در `SettingsPage.tsx` (Admin)
|
|
|
|
- به `FormValues` schema و `defaultValues` کلید `sms_price_rials` را اضافه کن (مثل `sms_panel_fee_rials`؛ نوع string، پیشفرض مثلاً `'500'`).
|
|
- در بخش `current.id === 'sms'` یک `Field` با `input type="number"` برای `sms_price_rials` اضافه کن (الگوی دقیقاً مشابه فیلد `sms_panel_fee_rials` در بخش financial):
|
|
```tsx
|
|
<Field label="هزینه هر پیامک" hint="مبلغ کسرشده از کیف پول به ازای هر پیامک ارسالی (ریال).">
|
|
<input {...register('sms_price_rials')} type="number" min={0} dir="ltr" className="input" style={{ maxWidth: 200 }} placeholder="500" />
|
|
</Field>
|
|
```
|
|
- مطمئن شو مقدار اولیه از `data` (پاسخ GET `/api/v1/admin/settings`) خوانده و در PATCH ارسال میشود (چون کل `FormValues` ارسال میشود، با افزودن به schema/defaults خودکار انجام میشود).
|
|
|
|
### ۴. رفع state کهنهٔ فرم تنظیمات در `SmsWalletPage.tsx`
|
|
|
|
بعد از ذخیرهٔ موفق، `localSettings` را ریست کن تا `currentSettings` به دادهٔ تازهٔ سرور برگردد (وضعیت تأیید متن، مقادیر نرمالشده):
|
|
|
|
```tsx
|
|
const saveMutation = useMutation({
|
|
mutationFn: (body: SmsSettings) => api.patch('/api/v1/sms/settings', body),
|
|
onSuccess: () => {
|
|
setLocalSettings(null); // ← افزوده شود
|
|
qc.invalidateQueries({ queryKey: ['sms-settings'] });
|
|
toast.success('تنظیمات ذخیره شد');
|
|
},
|
|
onError: (e: any) => toast.error(e.message),
|
|
});
|
|
```
|
|
|
|
- همچنین اگر لازم است که با هر بار تازهشدن `settingsData` هم state محلی صفر شود (برای جلوگیری از ماندگاری ویرایشهای ذخیرهنشده پس از رفچ)، یک `useEffect(() => { setLocalSettings(null); }, [settingsData])` اضافه کن.
|
|
- در بدنهٔ PATCH فقط فیلدهای موردنیاز فرستاده شوند (`reminder_enabled`, `reminder_hours_before`, `post_visit_enabled`, `post_visit_text`)؛ backend بقیه را نادیده میگیرد ولی برای تمیزی میتوان همینها را صریح فرستاد.
|
|
|
|
### ۵. دسترسی صفحهٔ کیفپول برای کاربرانِ بدون entity (بررسی و تصمیم)
|
|
|
|
`SmsWalletController::resolveEntity` فقط برای `ROLE_DOCTOR` و `ROLE_CLINIC` entity برمیگرداند؛ برای بقیه (از جمله ادمینِ بدون پروفایل، secretary، clinic_doctor) `['unknown', null]` → همهٔ endpointها `403 پروفایل یافت نشد` میدهند و صفحه «کار نمیکند».
|
|
|
|
- اگر صفحهٔ `/admin/sms-wallet` باید برای نقشهای دیگر (مثل `clinic_doctor`) هم کار کند، `resolveEntity` را گسترش بده تا آن نقشها را هم به doctor/clinic نگاشت کند.
|
|
- اگر کیفپول فقط برای doctor/clinic معنا دارد، در `SmsWalletPage.tsx` هنگام خطای 403 یک empty-state مناسب («کیف پول پیامک فقط برای پزشک/کلینیک فعال است») نمایش بده تا صفحه سفید/خراب نشود.
|
|
- تصمیم را بر اساس نقشهای واقعی پروژه بگیر و در گزارش ذکر کن.
|
|
|
|
## نکات مهم
|
|
|
|
- کلید تنظیم `sms_price_rials` باید همان واحد ریال باشد؛ balance→count و کسرِ هنگام ارسال باید از **یک** مقدار بخوانند تا ناسازگاری نشود.
|
|
- `SMS_PRICE_RIALS = 500` بهعنوان fallback بماند (نصبهای بدون مقدار config نشکنند).
|
|
- الگوهای موجود پنل: `Field` + `register` (React Hook Form)، پاسخها `data?.data`، تاریخها Unix.
|
|
- بعد از تغییر Backend/endpoint: `ddev exec php -l ...`، `ddev exec php bin/console cache:clear`، `ddev exec php vendor/bin/phpstan analyse src/Sms src/Config`؛ و بعد از تغییر TS: `ddev exec npx tsc --noEmit --project tsconfig.json`.
|
|
- طبق قانون پروژه، `docs/api/sms.md` (و در صورت لزوم `docs/api/admin.md` برای کلید تنظیم جدید) در همین session بهروز شود: افزودن `sms_price_rials` به لیست تنظیمات و توضیح استفادهٔ آن.
|
|
- Entity تغییر نمیکند → migration لازم نیست (فقط یک ردیف در جدول تنظیمات سایت که با set ساخته میشود).
|