# 画面設計書 22-カードアサイン画面

## 概要

本ドキュメントは、Fizzyアプリケーションにおけるカードアサイン画面の設計を記述したものです。カードアサイン画面は、カードに担当者を割り当てるためのポップアップ形式の画面です。

### 本画面の処理概要

**業務上の目的・背景**：チームでタスク管理を行う際、各カード（タスク）に担当者を明確にすることで、責任の所在を明らかにし、作業の進捗管理を容易にするための機能です。複数人を1つのカードにアサインでき（最大100人）、チームでの協働作業を支援します。

**画面へのアクセス方法**：カード詳細画面からアサインボタン（ショートカットキー `a`）を押すと、Turbo Frameとしてこの画面がポップアップ表示されます。URLは `/cards/:card_id/assignments/new` です。

**主要な操作・処理内容**：
1. 自分自身をカードにアサイン（「Me」ボタン、ショートカットキー `m`）
2. ボード内のユーザーをフィルター検索してアサイン
3. 既にアサインされているユーザーのアサイン解除（トグル動作）
4. キーボードナビゲーションによるユーザー選択

**画面遷移**：
- 遷移元：カード詳細画面（アサインボタンクリック）
- 遷移先：なし（Turbo Streamで画面更新、ポップアップは自動的に閉じる）

**権限による表示制御**：ログインユーザーのみアクセス可能です。カードにアクセス権限があるユーザーのみ操作可能です。アサイン可能なユーザーは、そのボードに参加しているアクティブなユーザーに限定されます。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 38 | 担当者アサイン | 主機能 | カードに担当者を割り当てる/解除する |
| 39 | 自己アサイン | 補助機能 | 現在のユーザーを自分でカードにアサイン |

## 画面種別

選択（ポップアップ）

## URL/ルーティング

| HTTPメソッド | URL | コントローラー#アクション |
|-------------|-----|------------------------|
| GET | `/cards/:card_id/assignments/new` | `cards/assignments#new` |
| POST | `/cards/:card_id/assignments` | `cards/assignments#create` |

## 入出力項目

| 項目名 | 入出力 | データ型 | 必須 | 説明 |
|-------|-------|---------|-----|------|
| search | 入力 | String | - | ユーザー名のフィルター検索文字列 |
| assignee_id | 入力 | UUID | 必須 | アサイン/解除対象のユーザーID |

## 表示項目

| 項目名 | データソース | 説明 |
|-------|-------------|------|
| ポップアップタイトル | 固定文字列 | 「Assign this to...」 |
| ショートカットキー表示 | 固定文字列 | `a`（タイトル横） |
| フィルター入力欄 | - | ユーザー名を絞り込むための入力欄 |
| 自分（Me） | Current.user | 現在のログインユーザー、ショートカット `m` |
| アサイン済みユーザー | @assigned_to | 既にカードにアサインされているユーザー一覧 |
| 未アサインユーザー | @users | ボード内でまだアサインされていないユーザー一覧 |
| 制限メッセージ | 固定文字列 | 「Maximum 100 assignees」（上限到達時） |

## イベント仕様

### 1-ユーザー選択（クリック/Enter）

ユーザー項目をクリックまたはEnterキーで選択すると、POSTリクエストが送信され、アサイン/解除がトグルされます。

**処理内容**：
1. `card_assignments_path`へPOSTリクエスト送信
2. `@card.toggle_assignment`でアサイン/解除を実行
3. Turbo Streamでカード表示を更新

### 2-フィルター入力

検索ボックスに文字を入力すると、リアルタイムでユーザー一覧がフィルタリングされます。

**処理内容**：
1. `filter`コントローラーの`filter`アクションが発火
2. JavaScriptで表示/非表示を切り替え（サーバー通信なし）

### 3-キーボードナビゲーション

上下キーでユーザー一覧を移動し、Enterで選択できます。

**処理内容**：
1. `navigable-list`コントローラーの`navigate`アクションが発火
2. 選択中の項目が視覚的にハイライト

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

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| 画面表示 | assignments | SELECT | カードのアサイン状態を取得 |
| 画面表示 | users | SELECT | ボード内のアクティブユーザーを取得 |
| ユーザー選択 | assignments | INSERT/DELETE | アサインの追加または削除 |
| ユーザー選択 | cards | UPDATE | updated_atの更新（touch） |

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

#### assignments

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | account_id | card.account_id | 自動設定 |
| INSERT | card_id | @card.id | 対象カード |
| INSERT | assignee_id | params[:assignee_id] | アサイン対象ユーザー |
| INSERT | assigner_id | Current.user.id | アサインを実行したユーザー |
| DELETE | - | card_id, assignee_id | 既存アサインの削除 |

#### cards

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | updated_at | 現在時刻 | assignments belongs_to :card, touch: true |

## メッセージ仕様

| 種別 | 条件 | メッセージ |
|------|------|----------|
| 警告 | アサイン上限到達 | Maximum 100 assignees |

## 例外処理

| 例外条件 | 処理内容 |
|---------|---------|
| カードが存在しない | 404エラーページを表示 |
| カードへのアクセス権限がない | 404エラーページを表示 |
| ユーザーがボードに存在しない | 422エラー（Unprocessable Entity） |
| アサイン上限超過 | バリデーションエラー、アサイン失敗 |

## 備考

- アサインの上限は`Assignment::LIMIT`定数で100人に設定されています
- Turbo Frameとして読み込まれ、`turbo_frame_tag @card, :assignment`でフレームを識別します
- `navigable-list`コントローラーで`focusOnSelection: false`、`actionableItems: true`が設定されています
- タッチデバイスではショートカットキーの表示が非表示になります（`hide-on-touch`クラス）
- ETTagによるキャッシュ制御が実装されています（`fresh_when etag: [ @users, @card.assignees ]`）

---

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

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

### 推奨読解順序

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

まず、アサイン機能に関連するモデルの構造を理解することが重要です。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | assignment.rb | `app/models/assignment.rb` | Assignmentモデルの構造、LIMITの定義、バリデーション |
| 1-2 | schema.rb | `db/schema.rb` | assignmentsテーブルの構造（132-142行目） |
| 1-3 | card.rb | `app/models/card.rb` | Cardモデル、Assignableモジュールのinclude |

**読解のコツ**: Assignmentモデルは`assignee`と`assigner`の2つのユーザー参照を持っています。`assignee`は担当者、`assigner`はアサインを実行した人です。

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

処理の起点となるコントローラーを特定します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | assignments_controller.rb | `app/controllers/cards/assignments_controller.rb` | new/createアクションの処理 |
| 2-2 | card_scoped.rb | `app/controllers/concerns/card_scoped.rb` | CardScopedモジュールによるカード取得 |

**主要処理フロー**:
1. **4行目**: `new` - アサイン済みユーザーと未アサインユーザーの取得
2. **5行目**: `@assigned_to` - 既にアサインされている（Current.user以外の）ユーザー
3. **6行目**: `@users` - まだアサインされていないユーザー
4. **10行目**: `create` - `toggle_assignment`でアサイン/解除

#### Step 3: ビューテンプレートを理解する

画面の表示内容を構成するビューファイルを確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | new.html.erb | `app/views/cards/assignments/new.html.erb` | メインビュー、Turbo Frame、コントローラー設定 |
| 3-2 | _user.html.erb | `app/views/cards/assignments/_user.html.erb` | ユーザー項目のパーシャル |

**主要処理フロー**:
- **1行目**: `turbo_frame_tag @card, :assignment` - Turbo Frameの定義
- **4行目**: `assignment_limit`コントローラーで上限制御
- **8行目**: `Assignment::LIMIT`で制限値を設定
- **20-27行目**: ユーザー一覧の描画（Me、アサイン済み、未アサイン）

#### Step 4: ルーティングを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | routes.rb | `config/routes.rb` | 94行目の`resources :assignments` |

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

```
GET /cards/:card_id/assignments/new
    |
    +-- Cards::AssignmentsController#new
           |
           +-- CardScoped#set_card (before_action)
           |      +-- Current.user.accessible_cards.find_by!
           |
           +-- CardScoped#set_board (before_action)
           |
           +-- @card.assignees (アサイン済みユーザー取得)
           +-- @board.users (ボード内ユーザー取得)
           |
           +-- render 'cards/assignments/new'
                  |
                  +-- render 'cards/assignments/_user' (複数回)

POST /cards/:card_id/assignments
    |
    +-- Cards::AssignmentsController#create
           |
           +-- @board.users.active.find(params[:assignee_id])
           +-- @card.toggle_assignment(user)
                  |
                  +-- Assignment.create! または destroy
           |
           +-- respond_to turbo_stream / json
```

### データフロー図

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

リクエスト               AssignmentsController             Turbo Stream
card_id ──────────────▶ set_card ──────────────────────▶ カード情報更新
assignee_id ──────────▶ toggle_assignment
                              |
                        ┌─────┴─────┐
                        ▼           ▼
                    未アサイン    アサイン済み
                        |           |
                        ▼           ▼
                    INSERT       DELETE
                   Assignment   Assignment
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| assignments_controller.rb | `app/controllers/cards/assignments_controller.rb` | ソース | アサイン機能のコントローラー |
| card_scoped.rb | `app/controllers/concerns/card_scoped.rb` | ソース | カード取得の共通処理 |
| new.html.erb | `app/views/cards/assignments/new.html.erb` | テンプレート | アサイン画面のビュー |
| _user.html.erb | `app/views/cards/assignments/_user.html.erb` | テンプレート | ユーザー項目パーシャル |
| assignment.rb | `app/models/assignment.rb` | ソース | Assignmentモデル |
| card.rb | `app/models/card.rb` | ソース | Cardモデル（Assignable含む） |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義 |
| schema.rb | `db/schema.rb` | 設定 | データベーススキーマ |
