diff --git a/app/app/Http/Resources/ProjectResource.php b/app/app/Http/Resources/ProjectResource.php index 7909e451..ba31b0b8 100644 --- a/app/app/Http/Resources/ProjectResource.php +++ b/app/app/Http/Resources/ProjectResource.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Http\Resources; use App\Models\Project; +use App\Services\Project\ProjectRuleMessages; use App\Services\Project\SupplierSnapshotGuard; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; @@ -25,6 +26,12 @@ class ProjectResource extends JsonResource pausedAt: $this->paused_at, ); + // Эпик 6.3: единый текст правила смены источника (из ProjectRuleMessages) — + // диалог подтверждения на экране тянет его отсюда, не дублирует строку в JS. + $sourceChangeMessage = $sourceLock['locked'] && $sourceLock['unlock_at'] !== null + ? (new ProjectRuleMessages)->sourceChanged($sourceLock['unlock_at']) + : null; + return [ 'id' => $this->id, 'name' => $this->name, @@ -57,6 +64,8 @@ class ProjectResource extends JsonResource 'source_locked' => $sourceLock['locked'], 'source_unlock_at' => $sourceLock['unlock_at']?->toIso8601String(), 'source_unlock_projected' => $sourceLock['projected'], + // Эпик 6.3: единый текст правила (бэкенд — источник истины для строк). + 'source_change_message' => $sourceChangeMessage, ]; } } diff --git a/app/resources/js/components/projects/ProjectDetailsDrawer.vue b/app/resources/js/components/projects/ProjectDetailsDrawer.vue index 3c632d40..6949e341 100644 --- a/app/resources/js/components/projects/ProjectDetailsDrawer.vue +++ b/app/resources/js/components/projects/ProjectDetailsDrawer.vue @@ -89,6 +89,11 @@ const sourceDirty = computed(() => { // Диалог подтверждения смены источника на залоченном проекте. const sourceConfirmOpen = ref(false); const sourceConfirmText = computed(() => { + // Эпик 6.3: единый текст правила из API (ProjectRuleMessages). Fallback — локальный. + const fromApi = props.project?.source_change_message; + if (fromApi) { + return `Мы уже ведём сбор на завтра. ${fromApi} Подтвердите смену источника.`; + } const d = formatLeadDate(props.project?.source_unlock_at); return ( 'Мы уже ведём сбор на завтра. Лиды по старому источнику придут завтра в любом случае. ' + diff --git a/app/resources/js/stores/projectsStore.ts b/app/resources/js/stores/projectsStore.ts index a7a87d79..f4357220 100644 --- a/app/resources/js/stores/projectsStore.ts +++ b/app/resources/js/stores/projectsStore.ts @@ -25,6 +25,8 @@ export interface Project { source_locked?: boolean; source_unlock_at?: string | null; source_unlock_projected?: boolean; + // Эпик 6.3: единый текст правила смены источника (из ProjectRuleMessages на бэкенде). + source_change_message?: string | null; } export const useProjectsStore = defineStore('projects', () => { diff --git a/app/resources/js/views/projects/NewProjectDialog.vue b/app/resources/js/views/projects/NewProjectDialog.vue index 04577739..15dbcd72 100644 --- a/app/resources/js/views/projects/NewProjectDialog.vue +++ b/app/resources/js/views/projects/NewProjectDialog.vue @@ -357,6 +357,11 @@ const sourceDirty = computed(() => { const sourceConfirmOpen = ref(false); const sourceConfirmText = computed(() => { + // Эпик 6.3: единый текст правила из API (ProjectRuleMessages). Fallback — локальный. + const fromApi = props.project?.source_change_message; + if (fromApi) { + return `Мы уже ведём сбор на завтра. ${fromApi} Подтвердите смену источника.`; + } const d = formatLeadDate(props.project?.source_unlock_at); return ( 'Мы уже ведём сбор на завтра. Лиды по старому источнику придут завтра в любом случае. ' + diff --git a/app/tests/Feature/Http/Resources/ProjectResourceSourceLockTest.php b/app/tests/Feature/Http/Resources/ProjectResourceSourceLockTest.php index c6cd77c9..2609cc75 100644 --- a/app/tests/Feature/Http/Resources/ProjectResourceSourceLockTest.php +++ b/app/tests/Feature/Http/Resources/ProjectResourceSourceLockTest.php @@ -43,6 +43,19 @@ it('project with no supplier links → source_locked false', function (): void { expect($res['source_unlock_projected'])->toBeFalse(); }); +it('отдаёт source_change_message (единый текст правила) для залоченного проекта', function (): void { + $res = (new ProjectResource(linkedProject(true)))->toArray(request()); + expect($res['source_change_message'])->toBeString() + ->and($res['source_change_message'])->toContain('по старому источнику'); +}); + +it('source_change_message = null когда источник не залочен', function (): void { + $tenant = Tenant::factory()->create(); + $p = Project::factory()->for($tenant)->create(['is_active' => true])->loadCount('supplierProjects'); + $res = (new ProjectResource($p))->toArray(request()); + expect($res['source_change_message'])->toBeNull(); +}); + it('paused linked project in grace → source_locked true, твёрдая дата, projected=false', function (): void { // На паузе час назад → в grace-окне (до следующего 21:00 МСК + 24ч). $res = (new ProjectResource(linkedProject(false, now()->subHour()->toDateTimeString())))->toArray(request()); diff --git a/app/tests/Frontend/ProjectDetailsDrawer.spec.ts b/app/tests/Frontend/ProjectDetailsDrawer.spec.ts index 3605c8b9..33344fee 100644 --- a/app/tests/Frontend/ProjectDetailsDrawer.spec.ts +++ b/app/tests/Frontend/ProjectDetailsDrawer.spec.ts @@ -351,6 +351,25 @@ describe('ProjectDetailsDrawer', () => { ); }); + it('диалог подтверждения берёт текст правила из source_change_message (единый источник)', async () => { + vi.mocked(axios.patch).mockReset(); + (axios.patch as unknown as ReturnType).mockResolvedValue({ data: { data: sampleProject } }); + const locked: Project = { + ...sampleProject, + signal_type: 'call', + signal_identifier: '79161112233', + source_locked: true, + source_change_message: 'Лиды по старому источнику придут до 27 июня, дальше — по новому.', + }; + const wrapper = mount(ProjectDetailsDrawer, { props: { project: locked } }); + await wrapper.get('[data-testid="pdd-signal-identifier"]').setValue('79990001122'); + await wrapper.get('[data-testid="pdd-save"]').trigger('click'); + await wrapper.vm.$nextTick(); + const dialog = wrapper.find('[data-testid="pdd-source-change-confirm"]'); + expect(dialog.exists()).toBe(true); + expect(dialog.text()).toContain('27 июня'); // дата из API-текста, не из JS-хардкода + }); + it('смена источника на незалоченном проекте сохраняется без подтверждения', async () => { vi.mocked(axios.patch).mockReset(); (axios.patch as unknown as ReturnType).mockResolvedValue({ data: { data: sampleProject } });