# 画面設計書 33-商談概要画面

## 概要

本ドキュメントは、Fat Free CRMにおける商談概要画面の設計仕様を記載する。本画面はユーザー別の商談パイプラインを俯瞰的に表示し、営業活動の進捗状況を把握するためのダッシュボード機能を提供する。

### 本画面の処理概要

本画面は、システム内の全ユーザーが担当する商談を一覧表示し、ユーザー別に商談をグループ化して表示する。営業チーム全体の商談パイプラインの可視化を目的とし、担当者ごとの商談状況を一目で確認できる。また、未割当の商談も別セクションで表示される。

**業務上の目的・背景**：営業マネージャーやチームリーダーが、チーム全体の商談状況を俯瞰的に把握するための画面である。各営業担当者がどの程度の商談を持っているか、パイプラインの偏りはないか、未割当の商談がどれだけ存在するかを確認し、リソース配分や進捗管理の判断材料とする。営業活動の効率化と売上予測の精度向上に貢献する。

**画面へのアクセス方法**：以下の方法でアクセス可能：
1. ナビゲーションメニューから「Opportunities Overview」を選択
2. URL `/users/opportunities_overview` に直接アクセス
3. ユーザープロフィール画面からのリンク

**主要な操作・処理内容**：
1. ユーザー別商談一覧の表示（アサインされた商談）
2. 各商談の詳細情報（金額、ステージ等）の確認
3. 未割当商談セクションの表示
4. 個別商談の詳細画面への遷移

**画面遷移**：
- 遷移元：ダッシュボード、ナビゲーションメニュー、ユーザープロフィール画面
- 遷移先：商談詳細画面、商談新規作成フォーム

**権限による表示制御**：認証済みユーザーのみアクセス可能。表示される商談は、current_userのmy scope（アクセス権限）に基づいてフィルタリングされる。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 33 | 商談サマリー（ユーザー別） | 主機能 | ユーザー別の商談パイプラインサマリー表示 |

## 画面種別

一覧 / サマリー

## URL/ルーティング

| HTTPメソッド | URL | コントローラ#アクション | 説明 |
|-------------|-----|----------------------|------|
| GET | /users/opportunities_overview | users#opportunities_overview | 商談概要表示 |

**ルーティング定義（config/routes.rb 22-29行目）**:
```ruby
devise_scope :user do
  resources :users, only: %i[index show] do
    collection do
      get :opportunities_overview
      match :auto_complete, via: %i[get post]
    end
  end
end
```

## 入出力項目

本画面は参照専用のため、入力項目はない。

## 表示項目

### ユーザーセクション

| 項目名 | データソース | 表示形式 | 備考 |
|--------|------------|---------|------|
| ユーザー名 | User#full_name | テキスト | セクションタイトル |

### 商談一覧（各ユーザーごと）

| 項目名 | データソース | 表示形式 | 備考 |
|--------|------------|---------|------|
| 商談名 | Opportunity#name | リンク | 詳細画面へ遷移 |
| 金額 | Opportunity#amount | 通貨 | 金額降順でソート |
| ステージ | Opportunity#stage | テキスト | ステージでソート |
| 取引先 | Opportunity#account | リンク | 関連取引先 |
| タグ | Opportunity#tags | タグ表示 | |

### 未割当商談セクション

| 項目名 | データソース | 表示形式 | 備考 |
|--------|------------|---------|------|
| セクションタイトル | - | テキスト | "Unassigned Opportunities" |
| 商談一覧 | @unassigned_opportunities | リスト | pipeline scope適用 |

## イベント仕様

### 1-画面表示

**トリガー**: 画面アクセス（GET /users/opportunities_overview）

**処理フロー**:
1. UsersController#opportunities_overview アクションが呼び出される
2. `User.have_assigned_opportunities` スコープで商談を持つユーザーを取得
3. `Opportunity.my(current_user).unassigned.pipeline` で未割当商談を取得
4. opportunities_overview.html.haml が描画される

**コントローラ処理（users_controller.rb 107-110行目）**:
```ruby
def opportunities_overview
  @users_with_opportunities = User.have_assigned_opportunities.order(:first_name)
  @unassigned_opportunities = Opportunity.my(current_user).unassigned.pipeline.order(:stage).includes(:account, :user, :tags)
end
```

### 2-商談詳細遷移

**トリガー**: 商談名リンクのクリック

**処理フロー**:
1. 該当商談の詳細画面（/opportunities/:id）に遷移

### 3-商談新規作成

**トリガー**: 商談作成リンクのクリック（#create_opportunity 領域）

**処理フロー**:
1. 商談新規作成フォームを表示（Ajax）

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

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| 画面表示 | users, opportunities, accounts, tags | SELECT | 商談情報を取得 |

### テーブル別クエリ詳細

#### users

| 操作 | 条件 | 備考 |
|-----|------|------|
| SELECT | INNER JOIN opportunities ON users.id = opportunities.assigned_to WHERE opportunities.stage <> 'lost' AND opportunities.stage <> 'won' | have_assigned_opportunities scope |

#### opportunities

| 操作 | 条件 | 備考 |
|-----|------|------|
| SELECT | assigned_to IS NULL AND stage NOT IN ('lost', 'won') | unassigned.pipeline scope |
| SELECT | user_id経由でアクセス権限チェック | my(current_user) scope |

## メッセージ仕様

| 種別 | メッセージキー | 表示内容 | 表示条件 |
|------|--------------|---------|---------|
| 情報 | no_opportunities_found | 商談が見つかりません | ユーザーも未割当商談も存在しない場合 |
| ラベル | unassigned_opportunities | 未割当商談 | 未割当商談セクションのタイトル |

## 例外処理

| 例外 | 対応 |
|------|------|
| 未認証アクセス | ログイン画面へリダイレクト |
| 権限なし | 403 Forbidden を返却 |

## 備考

- `pipeline` スコープにより、lost/won以外のステージの商談のみが表示される
- 商談は金額の降順でソートされ、高額案件が上位に表示される
- styles_for :opportunity により商談用のスタイルシートが読み込まれる
- _user_report パーシャルでユーザー別商談リストが描画される
- _opportunity パーシャル（opportunities/opportunity）で個別商談が描画される

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | user.rb | `app/models/users/user.rb` | have_assigned_opportunities スコープ（81-85行目） |
| 1-2 | opportunity.rb | `app/models/entities/opportunity.rb` | assigned_to, pipeline, unassigned スコープ |
| 1-3 | schema.rb | `db/schema.rb` | opportunitiesテーブル構造（327-346行目） |

**読解のコツ**: `have_assigned_opportunities` スコープ（user.rb 81-85行目）は、INNER JOINを使用してアサインされた商談を持つユーザーのみを抽出する。

```ruby
scope :have_assigned_opportunities, lambda {
  joins("INNER JOIN opportunities ON users.id = opportunities.assigned_to")
    .where("opportunities.stage <> 'lost' AND opportunities.stage <> 'won'")
    .select('DISTINCT(users.id), users.*')
}
```

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | routes.rb | `config/routes.rb` | opportunities_overview ルート（25行目） |
| 2-2 | users_controller.rb | `app/controllers/users_controller.rb` | opportunities_overview アクション（107-110行目） |

**主要処理フロー**:
1. **107行目**: `def opportunities_overview` アクション定義
2. **108行目**: `User.have_assigned_opportunities.order(:first_name)` でユーザー取得
3. **109行目**: `Opportunity.my(current_user).unassigned.pipeline.order(:stage).includes(:account, :user, :tags)` で未割当商談取得

#### Step 3: ビュー層を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | opportunities_overview.html.haml | `app/views/users/opportunities_overview.html.haml` | 全体構造（1-16行目） |
| 3-2 | _user_report.html.haml | `app/views/users/_user_report.html.haml` | ユーザー別商談表示（1-6行目） |
| 3-3 | _opportunity.html.haml | `app/views/opportunities/_opportunity.html.haml` | 個別商談表示 |

**主要処理フロー**:
- **1行目**: `styles_for :opportunity` で商談用スタイル読み込み
- **5行目**: `@users_with_opportunities.any?` で表示判定
- **7行目**: `_user_report` パーシャルをコレクション描画
- **9-14行目**: 未割当商談セクションを条件付き表示
- **16行目**: 商談が存在しない場合のメッセージ

#### Step 4: パーシャルの詳細を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | _user_report.html.haml | `app/views/users/_user_report.html.haml` | ユーザー名表示、商談リスト |

**主要処理フロー**:
- **1行目**: `dom_id(user_report)` で一意のDOM IDを生成
- **4行目**: `user_report.full_name` でユーザー名表示
- **6行目**: `user_report.assigned_opportunities.pipeline.order("opportunities.amount DESC")` で商談を金額降順で取得

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

```
リクエスト: GET /users/opportunities_overview
    │
    ├─ routes.rb (devise_scope)
    │      └─ get :opportunities_overview (25行目)
    │
    ├─ UsersController
    │      ├─ set_current_tab (before_action)
    │      ├─ check_authorization
    │      ├─ load_and_authorize_resource
    │      │
    │      └─ opportunities_overview (107-110行目)
    │             ├─ User.have_assigned_opportunities.order(:first_name)
    │             │      └─ INNER JOIN opportunities
    │             │
    │             └─ Opportunity.my(current_user).unassigned.pipeline
    │                    ├─ accessible_by(ability)
    │                    ├─ assigned_to IS NULL
    │                    └─ stage NOT IN ('lost', 'won')
    │
    └─ opportunities_overview.html.haml
           ├─ render partial: "_user_report", collection: @users_with_opportunities
           │      └─ render partial: "opportunities/opportunity", collection: ...
           │
           └─ #unassigned (未割当商談)
                  └─ render partial: "opportunities/opportunity", collection: @unassigned_opportunities
```

### データフロー図

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

HTTPリクエスト   ───▶ UsersController          ───▶ opportunities_overview.html.haml
GET /users/              │                            │
opportunities_overview   │                            ├─ ユーザー別商談リスト
                         │                            │      └─ _user_report.html.haml
                         ├─ @users_with_opportunities │
                         │      └─ User.have_assigned_opportunities
                         │                            └─ 未割当商談リスト
                         └─ @unassigned_opportunities      └─ #unassigned セクション
                                └─ Opportunity.unassigned.pipeline
```

```
[データ取得フロー]

users テーブル ──INNER JOIN──▶ opportunities テーブル
    │                              │
    ├─ id                          ├─ assigned_to
    ├─ first_name                  ├─ stage (!= 'lost', 'won')
    ├─ full_name                   ├─ amount
    │                              │
    └─ SELECT DISTINCT(users.id)   └─ ORDER BY amount DESC
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| opportunities_overview.html.haml | `app/views/users/opportunities_overview.html.haml` | ビュー | 商談概要画面 |
| _user_report.html.haml | `app/views/users/_user_report.html.haml` | パーシャル | ユーザー別商談表示 |
| _opportunity.html.haml | `app/views/opportunities/_opportunity.html.haml` | パーシャル | 個別商談表示 |
| users_controller.rb | `app/controllers/users_controller.rb` | コントローラ | opportunities_overview アクション |
| user.rb | `app/models/users/user.rb` | モデル | have_assigned_opportunities スコープ |
| opportunity.rb | `app/models/entities/opportunity.rb` | モデル | Opportunity モデル、スコープ定義 |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義 |
