> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cattix.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Google Ads CRUD та редагування кампаній

> Нові ендпоінти для управління ресурсами Google Ads (групи оголошень, оголошення, ключові слова, таргетинг) та редагування активних кампаній

<Note>
  This page is also available in [English](/guides/google-ads-crud).
</Note>

## Огляд

Цей реліз додає дві основні групи ендпоінтів:

1. **Google Ads Management CRUD** — створення, оновлення та видалення груп оголошень, оголошень (RSA), ключових слів, розкладу показів та таргетингу за локацією
2. **Редагування активних кампаній** — читання та оновлення налаштувань вже опублікованих кампаній Google Ads

Також є кілька **змін в існуючих GET ендпоінтах**, на які варто звернути увагу.

***

## Зміни в існуючих ендпоінтах

### Параметр `login_customer_id` більше не потрібний

Параметр `login_customer_id` більше не використовується жодним ендпоінтом
Google Ads. Бекенд тепер автоматично визначає його з бази даних на основі
`customer_id`.

Якщо ви зараз передаєте `login_customer_id` — нічого не зламається, він просто
ігнорується. Проте рекомендуємо видалити його з API-викликів для чистоти коду,
оскільки він більше не має жодного ефекту.

### Новий фільтр `campaign_id` на `/ads`

Ендпоінт `GET /ads` тепер приймає необов'язковий параметр `campaign_id` для фільтрації оголошень за кампанією. Це на додаток до існуючого фільтра `ad_group_id`.

```
GET /api/v1/google-ads/{customer_id}/ads?campaign_id=123456
```

### Послаблення фільтрів за статусом

Ендпоінти таблиць даних, які раніше повертали лише `ENABLED` ресурси, тепер
також повертають `PAUSED` ресурси. Це стосується кампаній, груп оголошень,
оголошень та ключових слів. Фронтенд має бути готовий відображати обидва статуси.

***

## Серверний пошук (`?search=`)

Усі GET ендпоінти таблиць даних тепер підтримують параметр `search`. Див.
окремий гайд [Пошук по таблицях](/guides/table-search-uk) для повної
специфікації API (поля пошуку по кожній таблиці, правила параметрів тощо).

Цей розділ фокусується на **реалізації пошуку на фронтенді**, базуючись на
тому, як це вже працює в `admin.cattix.com`.

### Два рівні пошуку

Адмін-панель реалізує пошук на двох рівнях:

1. **Пошуковий рядок у кожній таблиці** — текстове поле над таблицею, яке
   фільтрує конкретну таблицю через `?search=<term>`
2. **Глобальний пошуковий діалог** (`Cmd+K`) — відправляє той самий
   `?search=` запит до всіх 7 таблиць паралельно, показуючи результати
   згруповані за категоріями

Можна реалізувати один або обидва варіанти.

### Рекомендований паттерн реалізації

#### 1. Debounce вводу (300мс)

Не робіть API-виклик на кожне натискання клавіші. В адмінці використовується
debounce 300мс:

```typescript theme={null}
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)

const onSearchChange = (value: string) => {
  setQuery(value)
  if (debounceRef.current) clearTimeout(debounceRef.current)

  if (!value.trim()) {
    // Очистити результати одразу коли поле порожнє
    resetResults()
    return
  }

  debounceRef.current = setTimeout(() => {
    fetchWithSearch(value)
  }, 300)
}
```

#### 2. Захист від застарілих результатів (race condition)

Якщо користувач набирає "sho", а потім швидко змінює на "brand", відповідь
на "sho" може прийти після "brand". Використовуйте search-ID ref для
відкидання застарілих відповідей:

```typescript theme={null}
const searchIdRef = useRef(0)

const fetchWithSearch = (query: string) => {
  const currentId = ++searchIdRef.current

  api.getCampaigns({ customer_id, search: query }).then((res) => {
    if (searchIdRef.current !== currentId) return // застаріло — ігноруємо
    setResults(res.data)
  })
}
```

#### 3. Для глобального пошуку — паралельні запити

В адмінці всі 7 запитів категорій відправляються незалежно (не через
`Promise.all`), тому результати з'являються поступово:

```typescript theme={null}
const CATEGORIES = [
  "campaigns", "ad-groups", "ads", "keywords",
  "search-terms", "ad-schedule", "locations",
] as const

for (const category of CATEGORIES) {
  api[category]({ customer_id, search: query, limit: 5 })
    .then((res) => {
      if (searchIdRef.current !== currentId) return
      setState((prev) => ({
        ...prev,
        [category]: { loading: false, results: extract(category, res) },
      }))
    })
    .catch((err) => {
      if (searchIdRef.current !== currentId) return
      setState((prev) => ({
        ...prev,
        [category]: { loading: false, error: err.message, results: [] },
      }))
    })
}
```

Це дає приємний ефект поступового завантаження з індикатором прогресу
("Пошук... 3/7 завантажено").

#### 4. Стани UI для обробки

| Стан                | Що показувати                                               |
| ------------------- | ----------------------------------------------------------- |
| Клієнт не вибраний  | Вимкнене поле вводу, підказка вибрати акаунт                |
| Порожній запит      | Плейсхолдер ("Пошук кампаній, оголошень, ключових слів...") |
| Завантаження        | Скелетон-рядки або спінер на кожну категорію                |
| Часткові результати | Показати готові категорії, скелетон для очікуваних          |
| Немає результатів   | "Немає результатів для 'запит'"                             |
| Помилка             | Повідомлення про помилку для кожної категорії               |

#### 5. Навігація з глобального пошуку до таблиці

Коли користувач обирає результат у діалозі, перенаправте на сторінку з
таблицею з передзаповненим пошуком:

```typescript theme={null}
const handleSelect = (category: string) => {
  router.push(`/google-ads-data?tab=${category}&search=${encodeURIComponent(query)}`)
  closeDialog()
}
```

На сторінці даних зчитайте `search` з URL-параметрів і автоматично зробіть
запит при завантаженні.

### Референсна реалізація

Повна робоча реалізація знаходиться в `admin.cattix.com`:

| Файл                                      | Призначення                                                               |
| ----------------------------------------- | ------------------------------------------------------------------------- |
| `src/hooks/use-global-search.ts`          | Хук пошуку з debounce, паралельними запитами, захистом від race condition |
| `src/components/global-search-dialog.tsx` | UI діалогу `Cmd+K` з `cmdk`                                               |
| `src/lib/api/google-ads.ts`               | API-клієнт, що передає параметр `search`                                  |
| `src/app/admin/google-ads-data/page.tsx`  | Сторінка даних, що зчитує `?search=` з URL                                |

***

## Google Ads Management CRUD

Усі ендпоінти управління знаходяться під префіксом `/api/v1/google-ads/` і
потребують параметра `customer_id`.

Базовий шаблон URL:

```
POST/PATCH/PUT/DELETE /api/v1/google-ads/{ресурс}?customer_id={customer_id}
```

Усі ендпоінти потребують заголовок `Authorization: Bearer <token>`.

### Групи оголошень

#### Створити групу оголошень ([Swagger](/api-reference/google-ads-management/create-ad-group))

```
POST /api/v1/google-ads/ad-groups?customer_id={customer_id}
```

| Поле             | Тип     | Обов'язкове | Опис                                                                       |
| ---------------- | ------- | ----------- | -------------------------------------------------------------------------- |
| `campaign_id`    | integer | Так         | ID кампанії                                                                |
| `name`           | string  | Так         | Назва (1-256 символів)                                                     |
| `ad_group_type`  | string  | Ні          | `SEARCH_STANDARD` (за замовч.), `DISPLAY_STANDARD`, `SHOPPING_PRODUCT_ADS` |
| `status`         | string  | Ні          | `ENABLED` (за замовч.) або `PAUSED`                                        |
| `cpc_bid_micros` | integer | Ні          | CPC ставка в мікро (1 000 000 = \$1.00)                                    |

**Відповідь:** `AdGroupMutationResult`

```json theme={null}
{
  "success": true,
  "ad_group_id": 123456789,
  "resource_name": "customers/1234567890/adGroups/123456789",
  "error_code": null,
  "error_message": null
}
```

#### Оновити статус групи оголошень ([Swagger](/api-reference/google-ads-management/update-ad-group-status))

```
PATCH /api/v1/google-ads/ad-groups/{ad_group_id}/status?customer_id={customer_id}
```

Тіло: `{ "status": "ENABLED" | "PAUSED" }`

#### Видалити групу оголошень ([Swagger](/api-reference/google-ads-management/delete-ad-group))

```
DELETE /api/v1/google-ads/ad-groups/{ad_group_id}?customer_id={customer_id}
```

***

### Оголошення (RSA)

#### Створити RSA ([Swagger](/api-reference/google-ads-management/create-rsa))

```
POST /api/v1/google-ads/ads?customer_id={customer_id}
```

| Поле                    | Тип              | Обов'язкове | Опис                                                   |
| ----------------------- | ---------------- | ----------- | ------------------------------------------------------ |
| `ad_group_id`           | integer          | Так         | ID групи оголошень                                     |
| `final_url`             | string           | Так         | URL посадкової сторінки                                |
| `headlines`             | RSAAssetInput\[] | Так         | 3-15 заголовків (`text` + опціональний `pinned_field`) |
| `descriptions`          | RSAAssetInput\[] | Так         | 2-4 описи                                              |
| `display_path_1`        | string           | Ні          | Макс. 15 символів                                      |
| `display_path_2`        | string           | Ні          | Макс. 15 символів                                      |
| `final_mobile_url`      | string           | Ні          | Мобільний URL                                          |
| `tracking_url_template` | string           | Ні          | Шаблон трекінгу                                        |

#### Оновити RSA ([Swagger](/api-reference/google-ads-management/update-rsa))

```
PATCH /api/v1/google-ads/ads/{ad_id}?customer_id={customer_id}
```

Усі поля необов'язкові. Коли `headlines` або `descriptions` надані, вони **замінюють усі** існуючі.

#### Видалити оголошення ([Swagger](/api-reference/google-ads-management/delete-ad))

```
DELETE /api/v1/google-ads/ads/{ad_group_id}/{ad_id}?customer_id={customer_id}
```

***

### Ключові слова

#### Створити ключові слова ([Swagger](/api-reference/google-ads-management/create-keywords))

```
POST /api/v1/google-ads/keywords?customer_id={customer_id}
```

| Поле          | Тип             | Обов'язкове | Опис                                                                                   |
| ------------- | --------------- | ----------- | -------------------------------------------------------------------------------------- |
| `ad_group_id` | integer         | Так         | ID групи оголошень                                                                     |
| `keywords`    | KeywordInput\[] | Так         | Масив: `text` (обов.) + `match_type` (`EXACT`, `PHRASE`, `BROAD`, за замовч. `PHRASE`) |

#### Оновити ключове слово ([Swagger](/api-reference/google-ads-management/update-keyword))

```
PATCH /api/v1/google-ads/keywords/{ad_group_id}/{criterion_id}?customer_id={customer_id}
```

| Поле             | Тип     | Опис                    |
| ---------------- | ------- | ----------------------- |
| `status`         | string  | `ENABLED` або `PAUSED`  |
| `cpc_bid_micros` | integer | Нова CPC ставка в мікро |

#### Видалити ключові слова (пакетно) ([Swagger](/api-reference/google-ads-management/remove-keywords))

```
POST /api/v1/google-ads/keywords/remove?customer_id={customer_id}
```

***

### Розклад показів

#### Додати записи розкладу ([Swagger](/api-reference/google-ads-management/create-ad-schedule))

```
POST /api/v1/google-ads/ad-schedule?customer_id={customer_id}
```

| Поле          | Тип                     | Обов'язкове | Опис                                                                                |
| ------------- | ----------------------- | ----------- | ----------------------------------------------------------------------------------- |
| `campaign_id` | integer                 | Так         | ID кампанії                                                                         |
| `entries`     | AdScheduleEntryInput\[] | Так         | `day_of_week`, `start_hour` (0-23), `start_minute`, `end_hour` (0-24), `end_minute` |

#### Замінити весь розклад ([Swagger](/api-reference/google-ads-management/replace-ad-schedule))

```
PUT /api/v1/google-ads/ad-schedule?customer_id={customer_id}
```

Те саме тіло. **Замінює всі** існуючі записи розкладу.

#### Видалити записи розкладу ([Swagger](/api-reference/google-ads-management/delete-ad-schedule))

```
POST /api/v1/google-ads/ad-schedule/delete?customer_id={customer_id}
```

Тіло: `{ "campaign_id": ..., "resource_names": [...] }`

***

### Таргетинг за локацією

#### Додати локації ([Swagger](/api-reference/google-ads-management/add-locations))

```
POST /api/v1/google-ads/locations/targeting?customer_id={customer_id}
```

| Поле          | Тип                       | Обов'язкове | Опис                                                           |
| ------------- | ------------------------- | ----------- | -------------------------------------------------------------- |
| `campaign_id` | integer                   | Так         | ID кампанії                                                    |
| `locations`   | LocationTargetingEntry\[] | Так         | `geo_target_constant_id` + `target_type` (`INCLUDE`/`EXCLUDE`) |

#### Замінити всі локації ([Swagger](/api-reference/google-ads-management/replace-locations))

```
PUT /api/v1/google-ads/locations/targeting?customer_id={customer_id}
```

#### Видалити таргетинг локацій ([Swagger](/api-reference/google-ads-management/delete-location-targeting))

```
POST /api/v1/google-ads/locations/targeting/delete?customer_id={customer_id}
```

***

## Редагування активних кампаній

Ці ендпоінти дозволяють читати та оновлювати налаштування вже опублікованих кампаній, під префіксом `/api/v1/campaigns/live/`.

### Отримати налаштування кампанії ([Swagger](/api-reference/campaigns/get-live-campaign-settings))

```
GET /api/v1/campaigns/live/{customer_id}/{campaign_id}
```

Повертає повні налаштування кампанії для UI редагування:

```json theme={null}
{
  "campaign_id": 123456789,
  "campaign_name": "Brand Campaign",
  "status": "ENABLED",
  "daily_budget_micros": 50000000,
  "budget_resource_name": "customers/123/campaignBudgets/456",
  "bidding_strategy": { ... },
  "network_settings": { ... },
  "start_date": "2025-01-15",
  "end_date": null,
  "language_ids": [1000],
  "location_targets": [ ... ],
  "ad_schedule": { ... },
  "conversion_goals": [ ... ]
}
```

### Оновити налаштування кампанії ([Swagger](/api-reference/campaigns/patch-live-campaign-settings))

```
PATCH /api/v1/campaigns/live/{customer_id}/{campaign_id}
```

Усі поля необов'язкові — оновлюються лише надані поля.

<Warning>
  Поля таргетингу (`language_ids`, `location_targets`, `ad_schedule`,
  `conversion_goals`) використовують семантику **повної заміни**. Коли будь-яке
  з цих полів надано, існуючі значення повністю замінюються. Якщо поле пропущено,
  воно залишається без змін.
</Warning>

| Поле                  | Тип        | Опис                          |
| --------------------- | ---------- | ----------------------------- |
| `campaign_name`       | string     | Нова назва (1-256 символів)   |
| `status`              | string     | `ENABLED` або `PAUSED`        |
| `daily_budget_micros` | integer    | Денний бюджет в мікро         |
| `bidding_strategy`    | object     | Стратегія ставок              |
| `network_settings`    | object     | Налаштування мереж            |
| `language_ids`        | integer\[] | Мови (повна заміна)           |
| `location_targets`    | object\[]  | Локації (повна заміна)        |
| `ad_schedule`         | object     | Розклад (повна заміна)        |
| `start_date`          | string     | Дата початку (YYYY-MM-DD)     |
| `end_date`            | string     | Дата завершення (YYYY-MM-DD)  |
| `conversion_goals`    | object\[]  | Цілі конверсій (повна заміна) |

**Відповідь:**

```json theme={null}
{
  "success": true,
  "campaign_updated": true,
  "budget_updated": true,
  "targeting_updated": false,
  "conversion_goals_updated": false,
  "warnings": [],
  "errors": []
}
```

### Видалити кампанію ([Swagger](/api-reference/campaigns/remove-a-live-google-ads-campaign))

```
DELETE /api/v1/campaigns/live/{customer_id}/{campaign_id}
```

Повертає `204 No Content`. Це **незворотна** дія.

***

## Контрольний список міграції

<Steps>
  <Step title="Реалізуйте серверний пошук">
    Це, ймовірно, найбільше завдання для FE. Додайте пошуковий рядок у кожну
    таблицю та, опціонально, глобальний діалог пошуку `Cmd+K`. Див.
    [гайд з реалізації вище](#серверний-пошук-search) для паттернів
    (debounce, захист від race condition, поступове завантаження) та
    [референсний код admin.cattix.com](#референсна-реалізація).
  </Step>

  <Step title="Обробляйте PAUSED ресурси в таблицях">
    Таблиці тепер повертають і ENABLED, і PAUSED ресурси. Додайте візуальне
    розрізнення для призупинених елементів.
  </Step>

  <Step title="Побудуйте CRUD UI для груп оголошень, оголошень, ключових слів">
    Використовуйте нові ендпоінти управління для реалізації дій
    створення/редагування/видалення в таблицях.
  </Step>

  <Step title="Побудуйте сторінку редагування кампанії">
    Використовуйте `GET /campaigns/live/{cid}/{campaign_id}` для заповнення
    форми та `PATCH` для збереження змін.
  </Step>

  <Step title="Додайте фільтр campaign_id до таблиці оголошень">
    Ендпоінт `/ads` тепер підтримує `?campaign_id=` для фільтрації.
  </Step>

  <Step title="Приберіть login_customer_id з API-викликів (необов'язково)">
    Він більше не використовується і буде проігнорований, але його видалення
    зробить код чистішим.
  </Step>
</Steps>
