diff --git a/app/app/Http/Requests/UpdateProjectRequest.php b/app/app/Http/Requests/UpdateProjectRequest.php index 0d230557..052c643f 100644 --- a/app/app/Http/Requests/UpdateProjectRequest.php +++ b/app/app/Http/Requests/UpdateProjectRequest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Http\Requests; +use App\Models\Project; use Illuminate\Foundation\Http\FormRequest; class UpdateProjectRequest extends FormRequest @@ -16,7 +17,7 @@ class UpdateProjectRequest extends FormRequest public function rules(): array { // signal_type immutable: не валидируется в правилах, controller игнорирует поле - return [ + $rules = [ 'name' => ['sometimes', 'string', 'max:255'], 'daily_limit_target' => ['sometimes', 'integer', 'min:1', 'max:10000'], // Plan 6: subject-level regions[] заменил region_mask/region_mode на API-уровне. @@ -28,5 +29,23 @@ class UpdateProjectRequest extends FormRequest 'sms_senders.*' => ['string', 'max:11'], 'sms_keyword' => ['sometimes', 'nullable', 'string', 'min:1', 'max:50'], ]; + + // 18.05.2026 UX: редактирование источника (signal_identifier) для site/call. + // Регулярки соответствуют StoreProjectRequest (domain + 7\d{10}). + // signal_type immutable — берём из текущего проекта по route id. + $projectId = $this->route('id'); + if ($projectId !== null) { + $project = Project::find($projectId); + if ($project !== null) { + if ($project->signal_type === 'site') { + $rules['signal_identifier'] = ['sometimes', 'string', 'regex:/^[a-z0-9][a-z0-9\-]*(\.[a-z0-9][a-z0-9\-]*)*\.[a-z]{2,}$/i']; + } elseif ($project->signal_type === 'call') { + $rules['signal_identifier'] = ['sometimes', 'string', 'regex:/^7\d{10}$/']; + } + // sms: signal_identifier меняется через sms_senders/sms_keyword (см. выше) + } + } + + return $rules; } } diff --git a/app/app/Services/Project/ProjectService.php b/app/app/Services/Project/ProjectService.php index 4460cbe4..12fe9695 100644 --- a/app/app/Services/Project/ProjectService.php +++ b/app/app/Services/Project/ProjectService.php @@ -14,8 +14,9 @@ class ProjectService public function update(Project $project, array $data): Project { // Immutable fields — silently drop (don't 422) + // signal_identifier — теперь editable (18.05.2026 ux), валидируется в UpdateProjectRequest. unset( - $data['tenant_id'], $data['signal_type'], $data['signal_identifier'], + $data['tenant_id'], $data['signal_type'], $data['delivered_today'], $data['delivered_in_month'], $data['supplier_b1_project_id'], $data['supplier_b2_project_id'], $data['supplier_b3_project_id'], $data['archived_at'], @@ -31,7 +32,10 @@ class ProjectService ], 422)); } - $needsResync = array_key_exists('sms_senders', $data) || array_key_exists('sms_keyword', $data); + // Resync на смену любого источник-несущего поля — поставщику нужно знать актуальный домен/телефон/sms. + $needsResync = array_key_exists('sms_senders', $data) + || array_key_exists('sms_keyword', $data) + || array_key_exists('signal_identifier', $data); $project->update($data); diff --git a/app/phpstan-baseline.neon b/app/phpstan-baseline.neon index 80f92d5a..6d55aff9 100644 --- a/app/phpstan-baseline.neon +++ b/app/phpstan-baseline.neon @@ -1497,7 +1497,7 @@ parameters: - message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:actingAs\(\)\.$#' identifier: method.notFound - count: 8 + count: 12 path: tests/Feature/Plan5/Projects/ProjectsUpdateTest.php - diff --git a/app/resources/js/components/projects/ProjectDetailsDrawer.vue b/app/resources/js/components/projects/ProjectDetailsDrawer.vue index 36fc5c37..47b6cc68 100644 --- a/app/resources/js/components/projects/ProjectDetailsDrawer.vue +++ b/app/resources/js/components/projects/ProjectDetailsDrawer.vue @@ -16,6 +16,7 @@ interface FormState { delivery_days_mask: number; sms_senders: string[]; sms_keyword: string; + signal_identifier: string; } const form = reactive({ @@ -25,6 +26,7 @@ const form = reactive({ delivery_days_mask: 127, sms_senders: [], sms_keyword: '', + signal_identifier: '', }); const selectableRegions = REGIONS.filter((r) => r.code !== 0); @@ -37,6 +39,7 @@ function reseedFromProject(p: Project | null): void { form.delivery_days_mask = p.delivery_days_mask ?? 127; form.sms_senders = p.sms_senders ?? []; form.sms_keyword = p.sms_keyword ?? ''; + form.signal_identifier = p.signal_identifier ?? ''; } reseedFromProject(props.project); @@ -78,6 +81,10 @@ async function onSave(): Promise { regions: form.regions, delivery_days_mask: form.delivery_days_mask, }; + // 18.05.2026 ux: редактирование источника проекта. + if (props.project.signal_type === 'site' || props.project.signal_type === 'call') { + payload.signal_identifier = form.signal_identifier; + } if (props.project.signal_type === 'sms') { payload.sms_senders = form.sms_senders; payload.sms_keyword = form.sms_keyword; @@ -127,6 +134,54 @@ const dayLabels = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
{{ errors.name[0] }}
+ + + +
+ Источник — отправители SMS + +
{{ errors.sms_senders[0] }}
+ Ключевое слово (опционально) + +
{{ errors.sms_keyword[0] }}
+
+