feat/ui: текст правила смены источника — единый из API, без дублей в JS (Эпик 6.3)
ProjectResource.source_change_message = ProjectRuleMessages.sourceChanged (тот же текст, что in-app уведомление 6.2). Диалоги подтверждения (дроуэр + окно Редактировать) тянут его из API с fallback на локальный текст. Бэкенд — единственный источник строк правил, экран и колокольчик не расходятся. Проверено глазами (epic6-unified-rule-text-confirm.png). Тесты: ProjectResource 5/5, дроуэр 27/27, EditProjectDialog 7/7. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
'Мы уже ведём сбор на завтра. Лиды по старому источнику придут завтра в любом случае. ' +
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 (
|
||||
'Мы уже ведём сбор на завтра. Лиды по старому источнику придут завтра в любом случае. ' +
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -351,6 +351,25 @@ describe('ProjectDetailsDrawer', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('диалог подтверждения берёт текст правила из source_change_message (единый источник)', async () => {
|
||||
vi.mocked(axios.patch).mockReset();
|
||||
(axios.patch as unknown as ReturnType<typeof vi.fn>).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<typeof vi.fn>).mockResolvedValue({ data: { data: sampleProject } });
|
||||
|
||||
Reference in New Issue
Block a user