feat(sales): массовая смена площадки прогрева

POST /api/sales/ad-audience/channels — начальник меняет channels
у отмеченных фирм разом (гейт denyIfNotHead, менеджеру 403).
firms() теперь отдаёт channels в списке.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-07-19 19:43:16 +03:00
parent 7d4f973733
commit aaeab9c62b
3 changed files with 76 additions and 0 deletions
@@ -111,6 +111,7 @@ class SalesAdAudienceController extends Controller
return [
'id' => $firm->id,
'firm_name' => $firm->firm_name,
'channels' => $firm->channels,
'city' => $firm->city,
'phones_count' => $firm->phones->count(),
'state' => $phone === null ? 'stopped' : $phone->state,
@@ -169,6 +170,32 @@ class SalesAdAudienceController extends Controller
return response()->json(['prospect_id' => $prospect->id]);
}
/**
* POST /api/sales/ad-audience/channels массовая смена площадки (только head).
*
* Начальник отмечает строки галочками и жмёт «В Яндекс» / «В ВК» / «В оба».
* Состав в Яндексе подтянется сам ближайшей ночной заливкой она фильтрует
* номера по этому полю.
*/
public function channels(Request $request): JsonResponse
{
if (($resp = $this->denyIfNotHead($request)) !== null) {
return $resp;
}
/** @var array{firm_ids: list<int>, channels: string} $data */
$data = $request->validate([
'firm_ids' => ['required', 'array', 'min:1'],
'firm_ids.*' => ['required', 'integer'],
'channels' => ['required', 'string', 'in:yandex,vk,both'],
]);
$updated = SalesAdAudienceFirm::whereIn('id', $data['firm_ids'])
->update(['channels' => $data['channels']]);
return response()->json(['updated' => $updated]);
}
// ── private ──────────────────────────────────────────────────────────────
/**
+2
View File
@@ -308,6 +308,8 @@ Route::middleware(['admin-db', 'auth:sales', 'sales-portal'])->prefix('api/sales
// Task 7 (план v2): список фирм на прогреве + назначение менеджера (только head).
Route::get('/ad-audience/firms', [SalesAdAudienceController::class, 'firms']);
Route::post('/ad-audience/firms/{firm}/assign', [SalesAdAudienceController::class, 'assign'])->whereNumber('firm');
// Task 5 (план выбора площадки прогрева): массовая смена площадки (только head).
Route::post('/ad-audience/channels', [SalesAdAudienceController::class, 'channels']);
// managers, dashboard
});
@@ -232,3 +232,50 @@ it('повторное назначение той же фирмы отверг
->postJson("/api/sales/ad-audience/firms/{$firm->id}/assign", ['sales_user_id' => $this->manager->id])
->assertStatus(422);
});
// ── Task 5: массовая смена площадки прогрева ────────────────────────────────
it('начальник меняет площадку сразу у нескольких фирм', function () {
$first = SalesAdAudienceFirm::create([
'firm_name' => 'Первая', 'warmup_started_at' => now(), 'channels' => 'yandex',
]);
$second = SalesAdAudienceFirm::create([
'firm_name' => 'Вторая', 'warmup_started_at' => now(), 'channels' => 'yandex',
]);
$this->actingAs($this->head, 'sales')
->postJson('/api/sales/ad-audience/channels', [
'firm_ids' => [$first->id, $second->id],
'channels' => 'both',
])
->assertOk()
->assertJson(['updated' => 2]);
expect($first->fresh()->channels)->toBe('both')
->and($second->fresh()->channels)->toBe('both');
});
it('менеджеру менять площадку нельзя', function () {
$firm = SalesAdAudienceFirm::create([
'firm_name' => 'Чужая', 'warmup_started_at' => now(),
]);
$this->actingAs($this->manager, 'sales')
->postJson('/api/sales/ad-audience/channels', [
'firm_ids' => [$firm->id], 'channels' => 'vk',
])
->assertStatus(403);
expect($firm->fresh()->channels)->toBe('yandex');
});
it('в списке фирм видно, где греем', function () {
SalesAdAudienceFirm::create([
'firm_name' => 'Показательная', 'warmup_started_at' => now(), 'channels' => 'vk',
]);
$this->actingAs($this->head, 'sales')
->getJson('/api/sales/ad-audience/firms')
->assertOk()
->assertJsonFragment(['channels' => 'vk']);
});