# قیمت هر پیامک در تنظیمات + رفع تنظیمات ارسال پیامک کیف‌پول ## پروژه `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(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 ``` - مطمئن شو مقدار اولیه از `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 ساخته می‌شود).