CRM/backend/app/Support/SettingsCatalog.php

183 خطوط
26 KiB
PHP

<?php
namespace App\Support;
use App\Models\Setting;
class SettingsCatalog
{
public static function definitions(): array
{
return [
self::d('company_name', 'general', 'string', 'CRM فروش', 'نام سامانه', 'در هدر، عنوان صفحه و پاسخ عمومی تنظیمات استفاده می‌شود.', true, true, false, null, ['Header', 'document.title']),
self::d('logo_path', 'general', 'string', '', 'لوگو', 'بارگذاری فایل لوگو هنوز در همین صفحه پیاده‌سازی نشده است.', false, false, false, null, [], true),
self::d('brand_color', 'general', 'string', '#2563eb', 'رنگ اصلی برند', 'در پاسخ عمومی تنظیمات منتشر می‌شود؛ اعمال کامل روی theme در مرحله بعدی انجام می‌شود.', true, true, false, null, ['public settings']),
self::d('favicon_path', 'general', 'string', '', 'فاوآیکن', 'بارگذاری مستقیم فاوآیکن به‌زودی اضافه می‌شود.', false, false, false, null, [], true),
self::d('pwa_icon_path', 'general', 'string', '', 'آیکن PWA', 'بارگذاری مستقیم آیکن PWA به‌زودی اضافه می‌شود.', false, false, false, null, [], true),
self::d('default_language', 'general', 'string', 'fa', 'زبان پیش‌فرض', 'فعلاً رابط فارسی است؛ چندزبانه‌سازی کامل به‌زودی.', false, true, false, ['fa', 'en'], [], true),
self::d('timezone', 'general', 'string', 'Asia/Tehran', 'منطقه زمانی', 'برای نمایش و اعتبارسنجی زمان‌ها نگهداری می‌شود.', true, true, false, ['Asia/Tehran', 'UTC'], ['follow-up validation']),
self::d('datetime_format', 'general', 'string', 'jalali_datetime', 'قالب تاریخ و زمان', 'در رابط کاربری فارسی استفاده می‌شود.', false, true, false, ['jalali_datetime', 'gregorian_datetime'], [], true),
self::d('jalali_calendar_enabled', 'general', 'boolean', 'true', 'تقویم جلالی', 'ورودی‌های تاریخ فعلی بر مبنای جلالی هستند.', true, true, false, null, ['PersianDateInput']),
self::d('currency', 'general', 'string', 'IRR', 'واحد پول', 'برای فرصت‌های فروش و گزارش‌ها نگهداری می‌شود.', true, true, false, ['IRR', 'IRT', 'USD'], ['deals UI']),
self::d('working_days', 'general', 'json', 'sat,sun,mon,tue,wed', 'روزهای کاری', 'برای کنترل زمان‌بندی پیگیری استفاده می‌شود.', true, false, false, null, ['FollowUpController', 'CallService']),
self::d('working_hours_start', 'general', 'string', '09:00', 'شروع ساعت کاری', 'اگر محدودیت ساعت کاری فعال باشد، پیگیری خارج از این بازه رد می‌شود.', true, false, false, null, ['FollowUpController', 'CallService']),
self::d('working_hours_end', 'general', 'string', '18:00', 'پایان ساعت کاری', 'اگر محدودیت ساعت کاری فعال باشد، پیگیری خارج از این بازه رد می‌شود.', true, false, false, null, ['FollowUpController', 'CallService']),
self::d('custom_holidays', 'general', 'json', '', 'تعطیلات اختصاصی', 'برای جلوگیری از زمان‌بندی پیگیری در تعطیلات استفاده می‌شود.', true, false, false, null, ['FollowUpController']),
self::d('phone_mask_enabled', 'security', 'boolean', 'true', 'مخفی‌سازی شماره', 'در خروجی API برای کاربران بدون view_full_phone اعمال می‌شود.', true, true, false, null, ['MaskPhoneNumber']),
self::d('phone_mask_level', 'security', 'string', 'partial', 'سطح مخفی‌سازی شماره', 'partial فقط بخشی از شماره را مخفی می‌کند؛ full کل شماره را مخفی می‌کند.', true, false, false, ['partial', 'full'], ['MaskPhoneNumber']),
self::d('password_min_length', 'security', 'integer', '8', 'حداقل طول رمز', 'در ساخت کاربر، تغییر کاربر و پروفایل اعمال می‌شود.', true, false, false, null, ['AuthController', 'UserController']),
self::d('password_require_mixed_case', 'security', 'boolean', 'false', 'حروف بزرگ و کوچک در رمز', 'در اعتبارسنجی رمز اعمال می‌شود.', true, false, false, null, ['PasswordPolicy']),
self::d('password_require_numbers', 'security', 'boolean', 'false', 'عدد در رمز', 'در اعتبارسنجی رمز اعمال می‌شود.', true, false, false, null, ['PasswordPolicy']),
self::d('password_require_symbols', 'security', 'boolean', 'false', 'نماد در رمز', 'در اعتبارسنجی رمز اعمال می‌شود.', true, false, false, null, ['PasswordPolicy']),
self::d('login_attempt_limit', 'security', 'integer', '5', 'حداکثر تلاش ورود', 'مسیر ورود هنوز از throttle ثابت لاراول استفاده می‌کند.', false, false, false, null, [], true),
self::d('session_timeout_minutes', 'security', 'integer', '120', 'انقضای نشست', 'فعلاً از تنظیمات session لاراول پیروی می‌کند.', false, false, false, null, [], true),
self::d('two_factor_enabled', 'security', 'boolean', 'false', 'ورود دو مرحله‌ای', 'زیرساخت 2FA هنوز اضافه نشده است.', false, false, false, null, [], true),
self::d('active_device_management_enabled', 'security', 'boolean', 'false', 'مدیریت دستگاه‌های فعال', 'به‌زودی.', false, false, false, null, [], true),
self::d('audit_log_retention_days', 'security', 'integer', '365', 'نگهداری لاگ امنیتی', 'مقدار نگهداری می‌شود؛ پاکسازی زمان‌بندی‌شده به‌زودی.', false, false, false, null, [], true),
self::d('export_protection_enabled', 'security', 'boolean', 'true', 'محافظت خروجی', 'خروجی لید به مجوز export_leads و view_full_phone وابسته است.', true, false, false, null, ['LeadController@export']),
self::d('ip_allowlist', 'security', 'string', '', 'IP Allowlist', 'اعمال محدودیت IP به‌زودی.', false, false, false, null, [], true),
self::d('full_phone_visibility_permission', 'users_roles', 'string', 'view_full_phone', 'مجوز مشاهده کامل شماره', 'از permission matrix نقش‌ها مدیریت می‌شود و در API masking اعمال می‌شود.', true, false, false, null, ['MaskPhoneNumber']),
self::d('export_permission', 'users_roles', 'string', 'export_leads', 'مجوز خروجی گرفتن', 'از نقش‌ها مدیریت می‌شود و در خروجی لید اعمال می‌شود.', true, false, false, null, ['LeadController@export']),
self::d('import_permission', 'users_roles', 'string', 'import_leads', 'مجوز ورود داده', 'از نقش‌ها مدیریت می‌شود و در ورود داده اعمال می‌شود.', true, false, false, null, ['ImportBatchPolicy']),
self::d('delete_merge_permission', 'users_roles', 'string', 'delete_leads,merge_duplicates', 'مجوز حذف/ادغام', 'حذف لید و merge شرکت‌ها به مجوز وابسته است.', true, false, false, null, ['LeadPolicy', 'CompanyController@merge']),
self::d('recording_visibility_permission', 'users_roles', 'string', 'listen_recordings', 'مجوز شنیدن ضبط', 'در نمایش recording_url اعمال می‌شود.', true, false, false, null, ['MaskPhoneNumber', 'CallPolicy']),
self::d('supervisor_scope_own_team', 'users_roles', 'boolean', 'true', 'محدودیت سرپرست به تیم خود', 'در AccessControl برای لید، تماس، پیگیری و گزارش اعمال می‌شود.', true, false, false, null, ['AccessControl']),
self::d('force_password_change_supported', 'users_roles', 'boolean', 'false', 'اجبار تغییر رمز بعدی', 'ستون و جریان تغییر اجباری رمز هنوز اضافه نشده است.', false, false, false, null, [], true),
self::d('duplicate_phone_policy', 'lead', 'string', 'warn', 'سیاست تکراری تلفن', 'در ایجاد و ورود لید اعمال می‌شود.', true, false, false, ['allow', 'warn', 'block', 'merge_suggestion'], ['LeadController', 'ImportService']),
self::d('manage_pipeline_stages', 'lead', 'string', '/pipeline-stages', 'مدیریت مراحل قیف', 'API مدیریت مراحل وجود دارد و از نقش admin استفاده می‌کند.', true, false, false, null, ['PipelineStageController']),
self::d('manage_lead_statuses', 'lead', 'string', '/lead-statuses', 'مدیریت وضعیت‌های لید', 'API مدیریت وضعیت‌ها وجود دارد و از نقش admin استفاده می‌کند.', true, false, false, null, ['LeadStatusController']),
self::d('manage_call_results', 'lead', 'string', 'call_results', 'مدیریت نتایج تماس', 'نتایج تماس دیتابیسی هستند؛ UI مدیریت کامل به‌زودی.', false, false, false, null, ['CallController@results'], true),
self::d('manage_lost_reasons', 'lead', 'string', '/lost-reasons', 'مدیریت علت‌های شکست', 'API علت‌های شکست موجود است.', true, false, false, null, ['LostReasonController']),
self::d('assignment_strategy', 'lead', 'string', 'manual', 'استراتژی تخصیص', 'در ایجاد لید بدون مسئول استفاده می‌شود.', true, false, false, ['manual', 'round_robin', 'least_busy', 'rule_based'], ['LeadController']),
self::d('first_call_sla_hours', 'lead', 'integer', '4', 'SLA اولین تماس', 'مقدار برای گزارش و هشدار نگهداری می‌شود؛ هشدار خودکار به‌زودی.', false, false, false, null, [], true),
self::d('follow_up_sla_hours', 'lead', 'integer', '24', 'SLA پیگیری', 'در زمان پیش‌فرض پیگیری استفاده می‌شود.', true, false, false, null, ['CallService']),
self::d('max_stage_age_hours', 'lead', 'integer', '72', 'حداکثر زمان در مرحله', 'هشدار مرحله به‌زودی.', false, false, false, null, [], true),
self::d('lead_sources', 'lead', 'json', 'وب‌سایت,معرفی,نمایشگاه,کمپین', 'منابع لید', 'برای لیست‌های انتخابی آینده نگهداری می‌شود.', false, false, false, null, [], true),
self::d('lead_tags', 'lead', 'json', '', 'برچسب‌ها', 'مدیریت ساختاری برچسب‌ها به‌زودی.', false, false, false, null, [], true),
self::d('voip_provider', 'voip', 'string', 'mock', 'ارائه‌دهنده', 'در مدیریت تلفن اینترنتی برای انتخاب ارائه‌دهنده استفاده می‌شود.', true, false, false, ['mock', 'ami', 'api', 'socket'], ['VoIPManager']),
self::d('voip_ami_host', 'voip', 'string', '', 'نشانی AMI الستیکس', 'میزبان Asterisk/Elastix AMI، مثلا 192.168.1.10.', true, false, false, null, ['AmiProvider']),
self::d('voip_ami_port', 'voip', 'integer', '5038', 'پورت AMI', 'پورت پیش‌فرض AMI معمولا 5038 است.', true, false, false, null, ['AmiProvider']),
self::d('voip_ami_username', 'voip', 'string', '', 'نام کاربری AMI', 'نام کاربری مرکزی AMI؛ برای هر کاربر CRM رمز جداگانه ذخیره نمی‌شود.', true, false, false, null, ['AmiProvider']),
self::d('voip_ami_secret', 'voip', 'string', '', 'رمز AMI', 'رمز مرکزی AMI به صورت محرمانه نگهداری می‌شود.', true, false, true, null, ['AmiProvider']),
self::d('voip_ami_channel_technology', 'voip', 'string', 'SIP', 'نوع داخلی', 'برای الستیکس‌های قدیمی معمولا SIP و برای نسخه‌های جدیدتر PJSIP است.', true, false, false, ['SIP', 'PJSIP'], ['AmiProvider']),
self::d('voip_ami_context', 'voip', 'string', 'from-internal', 'Context تماس', 'Context مقصد برای Originate در AMI.', true, false, false, null, ['AmiProvider']),
self::d('voip_ami_caller_id_template', 'voip', 'string', 'CRM <{extension}>', 'قالب Caller ID', 'عبارت {extension} با شماره داخلی کاربر جایگزین می‌شود.', true, false, false, null, ['AmiProvider']),
self::d('voip_ami_timeout', 'voip', 'integer', '5', 'مهلت اتصال AMI', 'حداکثر زمان انتظار برای اتصال socket به AMI.', true, false, false, null, ['AmiProvider']),
self::d('voip_ami_originate_timeout', 'voip', 'integer', '30', 'مهلت زنگ خوردن داخلی', 'مهلت Originate بر حسب ثانیه.', true, false, false, null, ['AmiProvider']),
self::d('voip_ami_recording_url', 'voip', 'string', '', 'قالب لینک ضبط AMI', 'اگر لینک ضبط از الگو ساخته می‌شود، {id} با شناسه تماس جایگزین می‌شود.', true, false, false, null, ['AmiProvider']),
self::d('voip_api_base_url', 'voip', 'string', '', 'Base URL', 'در provider API استفاده می‌شود.', true, false, false, null, ['ApiProvider']),
self::d('voip_api_token', 'voip', 'string', '', 'Token', 'به صورت secret نگهداری و در API/Socket استفاده می‌شود.', true, false, true, null, ['ApiProvider']),
self::d('voip_api_call_path', 'voip', 'string', '/calls', 'مسیر API تماس', 'در provider API استفاده می‌شود.', true, false, false, null, ['ApiProvider']),
self::d('voip_api_status_path', 'voip', 'string', '/calls/{id}', 'Status endpoint', 'در provider API استفاده می‌شود.', true, false, false, null, ['ApiProvider']),
self::d('voip_api_recording_url', 'voip', 'string', '', 'Recording endpoint', 'برای ساخت لینک ضبط استفاده می‌شود.', true, false, false, null, ['ApiProvider']),
self::d('voip_webhook_path', 'voip', 'string', '/api/voip/webhook', 'Webhook endpoint', 'دریافت webhook به‌زودی.', false, false, false, null, [], true),
self::d('voip_socket_timeout', 'voip', 'integer', '5', 'Timeout', 'در اتصال socket استفاده می‌شود.', true, false, false, null, ['SocketProvider']),
self::d('call_recording_enabled', 'voip', 'boolean', 'false', 'ضبط تماس', 'در ساخت تماس و نمایش recording اعمال می‌شود.', true, false, false, null, ['CallService']),
self::d('recording_retention_days', 'voip', 'integer', '90', 'نگهداری ضبط', 'پاکسازی خودکار ضبط‌ها به‌زودی.', false, false, false, null, [], true),
self::d('minimum_call_duration_seconds', 'voip', 'integer', '10', 'حداقل مدت تماس', 'اعتبارسنجی duration به‌زودی.', false, false, false, null, [], true),
self::d('call_result_required', 'voip', 'boolean', 'true', 'ثبت نتیجه تماس الزامی', 'فرم‌های ثبت نتیجه در UI/API الزام نتیجه دارند.', true, false, false, null, ['CallController']),
self::d('default_follow_up_hours', 'follow_up', 'integer', '24', 'زمان پیش‌فرض پیگیری', 'برای ایجاد پیگیری پیش‌فرض استفاده می‌شود.', true, false, false, null, ['CallService']),
self::d('reminder_before_due_minutes', 'follow_up', 'integer', '30', 'یادآوری قبل از موعد', 'نوتیفیکیشن زمان‌بندی‌شده به‌زودی.', false, false, false, null, [], true),
self::d('overdue_alert_hours', 'follow_up', 'integer', '2', 'هشدار تأخیر', 'در گزارش عقب‌افتادگی نگهداری می‌شود.', false, false, false, null, [], true),
self::d('escalate_overdue_to_supervisor', 'follow_up', 'boolean', 'false', 'ارجاع تأخیر به سرپرست', 'به‌زودی.', false, false, false, null, [], true),
self::d('auto_follow_up_call_results', 'follow_up', 'json', 'بعداً تماس بگیرید,نیازمند پیگیری,معرفی شماره جدید', 'نتایج تماس نیازمند پیگیری', 'با requires_follow_up در call_results همگام می‌شود.', true, false, false, null, ['CallController']),
self::d('prevent_follow_up_outside_working_hours', 'follow_up', 'boolean', 'false', 'جلوگیری خارج از ساعت کاری', 'در ساخت پیگیری دستی و بعد از تماس اعمال می‌شود.', true, false, false, null, ['FollowUpController', 'CallService']),
self::d('in_app_notifications_enabled', 'notification', 'boolean', 'true', 'اعلان داخل برنامه', 'سیستم اعلان داخلی فعال است.', true, false, false, null, ['NotificationService']),
self::d('browser_notifications_enabled', 'notification', 'boolean', 'false', 'اعلان مرورگر/PWA', 'اجازه push/browser به‌زودی.', false, false, false, null, [], true),
self::d('new_lead_notification_enabled', 'notification', 'boolean', 'true', 'اعلان لید جدید', 'در تخصیص‌ها و آینده ایجاد لید استفاده می‌شود.', true, false, false, null, ['NotificationService']),
self::d('assigned_lead_notification_enabled', 'notification', 'boolean', 'true', 'اعلان تخصیص لید', 'در LeadService و ImportService استفاده می‌شود.', true, false, false, null, ['LeadService', 'ImportService']),
self::d('follow_up_notification_enabled', 'notification', 'boolean', 'true', 'اعلان پیگیری', 'در FollowUpController و CallService استفاده می‌شود.', true, false, false, null, ['FollowUpController', 'CallService']),
self::d('overdue_notification_enabled', 'notification', 'boolean', 'true', 'اعلان پیگیری عقب‌افتاده', 'در endpoint پیگیری‌های عقب‌افتاده استفاده می‌شود.', true, false, false, null, ['FollowUpController@overdue']),
self::d('quiet_hours_start', 'notification', 'string', '22:00', 'شروع سکوت اعلان', 'اعمال quiet hours به‌زودی.', false, false, false, null, [], true),
self::d('quiet_hours_end', 'notification', 'string', '08:00', 'پایان سکوت اعلان', 'اعمال quiet hours به‌زودی.', false, false, false, null, [], true),
self::d('import_required_fields', 'import_export', 'json', 'company,phone', 'فیلدهای الزامی ورود داده', 'در ورود فعلی company و phone الزامی هستند.', true, false, false, null, ['ImportController']),
self::d('import_max_file_size_mb', 'import_export', 'integer', '10', 'حداکثر حجم ورود داده', 'در بارگذاری فایل ورود داده اعمال می‌شود.', true, false, false, null, ['ImportController']),
self::d('import_allowed_file_types', 'import_export', 'json', 'xlsx,xls,csv', 'نوع فایل ورود داده', 'در بارگذاری فایل ورود داده اعمال می‌شود.', true, false, false, null, ['ImportController']),
self::d('export_phone_mode', 'import_export', 'string', 'permission_based', 'حالت شماره در خروجی', 'full فقط با view_full_phone؛ masked برای بقیه.', true, false, false, ['permission_based', 'masked_only'], ['LeadController@export']),
self::d('import_rollback_allowed', 'import_export', 'boolean', 'true', 'بازگردانی ورود داده', 'در ساخت دسته ورود و بازگردانی ورود داده استفاده می‌شود.', true, false, false, null, ['ImportController']),
self::d('mapping_presets', 'import_export', 'json', '', 'الگوهای نگاشت', 'الگوهای نگاشت به‌زودی.', false, false, false, null, [], true),
self::d('attachment_max_file_size_mb', 'import_export', 'integer', '10', 'حداکثر حجم فایل پیوست', 'در بارگذاری فایل‌های لید/شرکت/فرصت اعمال می‌شود.', true, false, false, null, ['AttachmentController']),
self::d('attachment_allowed_file_types', 'import_export', 'json', 'pdf,jpg,jpeg,png,doc,docx,xls,xlsx,txt', 'نوع فایل پیوست', 'در بارگذاری فایل‌های پیوست اعمال می‌شود.', true, false, false, null, ['AttachmentController']),
self::d('daily_call_target', 'reports', 'integer', '40', 'هدف تماس روزانه', 'در داشبورد کارشناس و هشدارهای سرپرست/مدیریت استفاده می‌شود.', true, false, false, null, ['DashboardService']),
self::d('successful_call_target', 'reports', 'integer', '15', 'هدف تماس موفق', 'در پیشرفت KPI کارشناس استفاده می‌شود.', true, false, false, null, ['DashboardService']),
self::d('follow_up_target', 'reports', 'integer', '20', 'هدف پیگیری', 'در پیشرفت KPI کارشناس استفاده می‌شود.', true, false, false, null, ['DashboardService']),
self::d('conversion_target_percent', 'reports', 'integer', '20', 'هدف تبدیل', 'برای مقایسه مدیریتی نرخ تبدیل نگهداری و در گزارش‌ها قابل استفاده است.', true, false, false, null, ['ReportService', 'DashboardService']),
self::d('default_report_range', 'reports', 'string', 'month', 'بازه پیش‌فرض گزارش', 'اعمال پیش‌فرض گزارش به‌زودی.', false, false, false, ['today', 'week', 'month', 'quarter'], [], true),
self::d('pwa_enabled', 'pwa', 'boolean', 'true', 'فعال بودن PWA', 'manifest/service worker موجود است؛ خاموش کردن runtime به‌زودی.', false, true, false, null, [], true),
self::d('pwa_install_banner_text', 'pwa', 'string', 'نصب CRM فروش', 'متن بنر نصب PWA', 'در بنر نصب PWA نمایش داده می‌شود.', false, true, false, null, [], true),
self::d('pwa_theme_color', 'pwa', 'string', '#2563eb', 'رنگ theme', 'در پاسخ عمومی منتشر می‌شود؛ تزریق manifest به‌زودی.', false, true, false, null, [], true),
self::d('pwa_status_bar_color', 'pwa', 'string', '#ffffff', 'رنگ نوار وضعیت', 'اعمال کامل به‌زودی.', false, true, false, null, [], true),
self::d('ui_density', 'pwa', 'string', 'comfortable', 'حالت نمایش', 'compact/comfortable در UI آینده اعمال می‌شود.', false, true, false, ['compact', 'comfortable'], [], true),
self::d('default_landing_page', 'pwa', 'string', '/', 'صفحه پیش‌فرض', 'redirect پویا به‌زودی.', false, true, false, ['/', '/leads', '/crm', '/agent-mobile/today'], [], true),
self::d('offline_cache_duration_hours', 'pwa', 'integer', '24', 'مدت cache آفلاین', 'service worker پویا به‌زودی.', false, false, false, null, [], true),
self::d('push_permission_text', 'pwa', 'string', 'برای دریافت یادآوری‌ها اعلان‌ها را فعال کنید.', 'متن اجازه push', 'push notification به‌زودی.', false, true, false, null, [], true),
];
}
public static function ensureDefaults(): void
{
foreach (self::definitions() as $definition) {
$setting = Setting::firstOrNew(['key' => $definition['key']]);
$setting->fill([
'value' => $setting->exists ? $setting->value : $definition['default_value'],
'default_value' => $definition['default_value'],
'group' => $definition['group'],
'type' => $definition['type'],
'allowed_values' => $definition['allowed_values'],
'is_secret' => $definition['is_secret'],
'is_public' => $definition['is_public'],
'is_runtime_enforced' => $definition['is_runtime_enforced'],
'description' => $definition['hint'],
'validation_rules' => self::rulesFor($definition),
])->save();
}
}
public static function metaFor(string $key): ?array
{
foreach (self::definitions() as $definition) {
if ($definition['key'] === $key) {
return $definition;
}
}
return null;
}
private static function d(string $key, string $group, string $type, string $default, string $label, string $hint, bool $enforced, bool $public = false, bool $secret = false, ?array $allowed = null, array $usedBy = [], bool $comingSoon = false): array
{
return [
'key' => $key,
'group' => $group,
'type' => $type,
'default_value' => $default,
'label' => $label,
'hint' => $hint,
'allowed_values' => $allowed,
'is_runtime_enforced' => $enforced,
'is_public' => $public,
'is_secret' => $secret,
'used_by' => $usedBy,
'coming_soon' => $comingSoon,
];
}
private static function rulesFor(array $definition): array
{
return match ($definition['type']) {
'boolean' => ['in:true,false,1,0'],
'integer' => ['integer'],
'json' => ['nullable', 'string'],
default => ['nullable', 'string'],
};
}
}