# 画面設計書 70-プロモーション追加画面

## 概要

本ドキュメントは、Medusa Admin Dashboard のプロモーション追加画面について、画面仕様・機能・データ処理を定義した画面設計書です。

### 本画面の処理概要

この画面では、キャンペーンに既存のプロモーションを追加することができます。プロモーション一覧から複数選択して、一括でキャンペーンに紐付けます。

**業務上の目的・背景**：キャンペーンに関連するプロモーションを追加するための画面です。プロモーション一覧をテーブル形式で表示し、チェックボックスで複数選択できます。既に他のキャンペーンに紐付いているプロモーションや、通貨が一致しないプロモーションは選択できないよう制御されています。

**画面へのアクセス方法**：キャンペーン詳細画面のプロモーションセクションから「プロモーションを追加」ボタンをクリックすることで、フルページモーダル形式でこの画面が表示されます。URL: `/campaigns/:id/add-promotions`

**主要な操作・処理内容**：
1. プロモーション一覧を表示
2. 検索・フィルタでプロモーションを絞り込み
3. チェックボックスでプロモーションを選択
4. 保存ボタンで選択したプロモーションをキャンペーンに追加

**画面遷移**：
- 遷移元：キャンペーン詳細画面（`/campaigns/:id`）
- 遷移先：追加成功時はモーダルを閉じてキャンペーン詳細画面に戻る、キャンセル時も同様

**権限による表示制御**：認証済みの管理者ユーザーのみがアクセス可能です。キャンペーン管理権限を持つユーザーが操作できます。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 38 | キャンペーン管理 | 主機能 | キャンペーンへのプロモーション追加 |
| 36 | プロモーションCRUD | 補助機能 | プロモーション一覧の表示 |

## 画面種別

登録（フルページモーダル形式）

## URL/ルーティング

`/campaigns/:id/add-promotions`

## 入出力項目

| 項目名 | 項目ID | 型 | 必須 | 入力/出力 | 説明 |
|--------|--------|-----|------|-----------|------|
| プロモーションID一覧 | promotion_ids | string[] | ○ | 入力 | 1つ以上選択必須 |

## 表示項目

| 項目名 | 説明 |
|--------|------|
| プロモーション一覧テーブル | 選択可能なプロモーション一覧 |
| チェックボックス | 各行の選択状態 |
| 検索フィールド | プロモーションの検索（自動フォーカス） |
| フィルター | プロモーションのフィルタリング |
| ソート | code/type/created_at/updated_atでソート可能 |
| ページネーション | 50件ずつ表示 |
| キャンセルボタン | モーダルを閉じる |
| 保存ボタン | 選択したプロモーションを追加 |
| エラーヒント | 選択が必要な場合のエラーメッセージ |

## テーブル列

| 列名 | 説明 |
|------|------|
| チェックボックス | 選択状態（無効化条件あり） |
| プロモーション情報 | プロモーションテーブル列（code等） |

## チェックボックス無効化条件

以下の場合、チェックボックスが無効化される：
1. **既に現在のキャンペーンに追加済み**: ツールチップ「既に追加されています」
2. **他のキャンペーンに紐付け済み**: ツールチップ「{campaign_name}に追加されています」
3. **通貨不一致**（spendタイプの場合）: ツールチップ「通貨が一致しません」

## イベント仕様

### 1-保存ボタン押下

**処理フロー**：
1. フォームバリデーションを実行（1つ以上選択が必要）
2. `useAddOrRemoveCampaignPromotions` フックを使用してAPIを呼び出し
3. 成功時：トースト通知を表示し、モーダルを閉じる
4. 失敗時：エラーメッセージをトーストで表示

**API呼び出し**：
- エンドポイント: `POST /admin/campaigns/:id/promotions`
- リクエストボディ:
```json
{
  "add": [promotion_id, ...]
}
```

### 2-キャンセルボタン押下

**処理フロー**：
1. 選択状態を破棄
2. モーダルを閉じてキャンペーン詳細画面に戻る

### 3-チェックボックス選択

**処理フロー**：
1. rowSelectionステートを更新
2. フォームのpromotion_idsフィールドを更新

### 4-検索・フィルタ・ソート

**処理フロー**：
1. usePromotionTableQueryでクエリパラメータを更新
2. usePromotionsでプロモーション一覧を再取得

## データベース更新仕様

### 操作別データベース影響一覧

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| 保存ボタン押下 | promotion | UPDATE | campaign_idを設定 |

### テーブル別更新項目詳細

#### promotion

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | campaign_id | キャンペーンID | 選択されたプロモーション |

## メッセージ仕様

| メッセージID | 種別 | メッセージ内容 | 表示条件 |
|-------------|------|---------------|----------|
| MSG-001 | 成功 | {count}件のプロモーションを追加しました | 追加成功時 |
| MSG-002 | エラー | 1つ以上のプロモーションを選択してください | 未選択時 |
| MSG-003 | エラー | {エラーメッセージ} | API通信エラー時 |
| MSG-004 | ヒント | 既に追加されています | 現在のキャンペーンに追加済み |
| MSG-005 | ヒント | {campaign_name}に追加されています | 他のキャンペーンに追加済み |
| MSG-006 | ヒント | 通貨が一致しません | 通貨不一致 |

## 例外処理

| 例外 | 処理内容 |
|------|---------|
| キャンペーンが見つからない | エラーをスローしてエラー画面を表示 |
| API通信エラー | エラーメッセージをtoastで表示 |
| バリデーションエラー | Hintコンポーネントでエラーを表示 |

## 備考

- フルページモーダル形式（RouteFocusModal）で表示される
- ページサイズは50件
- 検索フィールドは自動フォーカス
- DataTableコンポーネントを使用
- 予算タイプがspendの場合、通貨が一致しないプロモーションは選択不可
- 既に他のキャンペーンに紐付いているプロモーションも選択不可
- addOrRemoveCampaignPromotionsWorkflowを使用

---

## コードリーディングガイド

本画面を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | add-campaign-promotions-form.tsx | `packages/admin/dashboard/src/routes/campaigns/add-campaign-promotions/components/add-campaign-promotions-form.tsx` | AddCampaignPromotionsSchema（28-30行目）でフォームの構造を確認 |

**読解のコツ**: promotion_idsは文字列配列で、min(1)の制約により1つ以上の選択が必要です。

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | add-campaign-promotions.tsx | `packages/admin/dashboard/src/routes/campaigns/add-campaign-promotions/add-campaign-promotions.tsx` | RouteFocusModalレイアウトとキャンペーンデータのロード |

**主要処理フロー**:
1. **7行目**: useParamsでキャンペーンIDを取得
2. **8行目**: useCampaignでキャンペーン詳細を取得
3. **14-18行目**: RouteFocusModalでフルページモーダルをレンダリング
4. **16行目**: AddCampaignPromotionsFormコンポーネントを表示

#### Step 3: フォームコンポーネントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | add-campaign-promotions-form.tsx | `packages/admin/dashboard/src/routes/campaigns/add-campaign-promotions/components/add-campaign-promotions-form.tsx` | フォームロジックと送信処理 |

**主要処理フロー**:
- **40-43行目**: useFormでフォーム初期化（空配列をデフォルト値に設定）
- **46-48行目**: useAddOrRemoveCampaignPromotionsでミューテーション準備
- **49行目**: rowSelectionステートで選択状態管理
- **51-61行目**: updater関数でrowSelection変更時にpromotion_idsを更新
- **73-92行目**: useDataTableでテーブル設定
- **94-109行目**: handleSubmitで送信処理

#### Step 4: チェックボックス列のカスタマイズ

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | add-campaign-promotions-form.tsx | `packages/admin/dashboard/src/routes/campaigns/add-campaign-promotions/components/add-campaign-promotions-form.tsx` | useColumns（168-260行目）でカスタムチェックボックス列の実装 |

**主要処理フロー**:
- **188-210行目**: セル内のロジック（無効化条件の判定）
- **196-205行目**: isTypeSpend、isAdded、isAddedToADiffCampaign、currencyMismatchの判定
- **218-251行目**: 条件に応じたTooltip表示

### プログラム呼び出し階層図

```
AddCampaignPromotions (add-campaign-promotions.tsx)
    │
    ├─ useCampaign() - キャンペーン詳細取得
    │      └─ sdk.admin.campaign.retrieve()
    │             └─ GET /admin/campaigns/:id
    │
    └─ AddCampaignPromotionsForm (add-campaign-promotions-form.tsx)
           │
           ├─ useForm() - フォーム状態管理
           │
           ├─ usePromotions() - プロモーション一覧取得
           │      └─ sdk.admin.promotion.list()
           │             └─ GET /admin/promotions
           │
           ├─ useDataTable() - テーブル状態管理
           │
           ├─ usePromotionTableColumns() - 基本列定義
           │
           ├─ usePromotionTableFilters() - フィルター定義
           │
           └─ useAddOrRemoveCampaignPromotions() - 紐付けミューテーション
                  └─ sdk.admin.campaign.batchPromotions()
                         └─ POST /admin/campaigns/:id/promotions
                                └─ addOrRemoveCampaignPromotionsWorkflow
```

### データフロー図

```
[入力]               [処理]                    [出力]

URLパラメータ ────▶ useCampaign() ────▶ キャンペーン詳細
     (id)                                      ↓
                    usePromotions() ──▶ プロモーション一覧
                                              ↓
                    useDataTable() ───▶ テーブル表示
                                              ↓
チェックボックス ───▶ updater() ───────▶ rowSelection更新
                          ↓
                    setValue() ───────▶ promotion_ids更新
                                              ↓
保存ボタン ──────────▶ handleSubmit ───▶ バリデーション
                                              ↓
                    useAddOrRemove...() ▶ API追加
                                              ↓
                    toast.success() ──▶ 成功通知
                                              ↓
                    handleSuccess() ──▶ モーダル閉じる
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| add-campaign-promotions.tsx | `packages/admin/dashboard/src/routes/campaigns/add-campaign-promotions/add-campaign-promotions.tsx` | ソース | エントリーポイント |
| add-campaign-promotions-form.tsx | `packages/admin/dashboard/src/routes/campaigns/add-campaign-promotions/components/add-campaign-promotions-form.tsx` | ソース | フォームコンポーネント |
| use-promotion-table-columns.tsx | `packages/admin/dashboard/src/hooks/table/columns/use-promotion-table-columns.tsx` | ソース | プロモーションテーブル列定義 |
| use-promotion-table-filters.tsx | `packages/admin/dashboard/src/hooks/table/filters/use-promotion-table-filters.tsx` | ソース | フィルター定義 |
| use-promotion-table-query.tsx | `packages/admin/dashboard/src/hooks/table/query/use-promotion-table-query.tsx` | ソース | テーブルクエリ |
| campaigns.tsx | `packages/admin/dashboard/src/hooks/api/campaigns.tsx` | ソース | APIフック |
| promotions.tsx | `packages/admin/dashboard/src/hooks/api/promotions.tsx` | ソース | プロモーションAPIフック |
| route.ts | `packages/medusa/src/api/admin/campaigns/[id]/promotions/route.ts` | ソース | APIエンドポイント |
