# 機能設計書 120-年間レビュー

## 概要

本ドキュメントは、Chatwootの年間レビュー機能の設計を記述する。この機能は、特定年度のサポート活動を振り返るための統計データを生成し、ユーザー個人のサポートパフォーマンスを要約するレポーティング機能である。

### 本機能の処理概要

年間レビュー機能は、指定された年度における特定ユーザー（エージェント）の会話対応実績を集計し、年間の総会話数、最も忙しかった日、平均応答時間などのサマリー情報を提供する機能である。このデータは「サポートパーソナリティ」として、エージェント個人のパフォーマンス振り返りに活用される。

**業務上の目的・背景**：年末や年度末において、エージェント個人のサポート活動を振り返ることは、モチベーション向上や自己成長の認識に重要である。年間レビュー機能により、「今年何件の会話に対応したか」「最も忙しかった日はいつか」「平均応答時間はどうだったか」といった個人の実績を可視化できる。

**機能の利用シーン**：
- 年末の個人パフォーマンス振り返り
- 年間表彰や評価のデータ参照
- モチベーション向上のための達成実績確認
- フロントエンドのYear in Reviewモーダル表示

**主要な処理内容**：
1. 指定年度と現在ユーザーに基づいてYearInReviewBuilderを呼び出し
2. 年間の総会話数を集計
3. 最も忙しかった日（会話数が最多の日）を特定
4. 平均初回応答時間を算出
5. 結果をユーザーのui_settingsにキャッシュ
6. JSON形式でレポートデータを返却

**関連システム・外部連携**：特になし（内部データの集計機能）

**権限による制御**：ログイン済みユーザーのみがアクセス可能（自身のデータのみ参照）

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | Year in Reviewモーダル | 主機能 | 年次統計の表示 |

## 機能種別

データ集計・レポート出力

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| year | Integer | No | 対象年度（デフォルト: 2025） | 数値 |

### 入力データソース

- HTTPリクエストパラメータ（API呼び出し時）
- Current.account（認証済みユーザーのアカウント情報）
- Current.user（認証済みユーザー情報）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| year | Integer | 対象年度 |
| total_conversations | Integer | 年間の総会話数 |
| busiest_day | Hash | 最も忙しかった日の情報 |
| busiest_day.date | String | 日付（"Jan 15"形式） |
| busiest_day.count | Integer | その日の会話数 |
| support_personality | Hash | サポートパーソナリティ情報 |
| support_personality.avg_response_time_seconds | Integer | 平均応答時間（秒） |

### 出力先

- JSON形式：API V2エンドポイント（`/api/v2/accounts/:account_id/year_in_review`）

## 処理フロー

### 処理シーケンス

```
1. API呼び出し（GET /api/v2/accounts/:account_id/year_in_review）
   └─ YearInReviewsController#show アクションが起動

2. キャッシュチェック
   └─ user.ui_settingsに既存のキャッシュデータがあるか確認

3a. キャッシュヒット時
   └─ キャッシュデータをそのまま返却

3b. キャッシュミス時
   └─ YearInReviewBuilder でデータを集計
       └─ 年間会話数、最多日、平均応答時間を算出

4. キャッシュ保存
   └─ 生成データをuser.ui_settingsに保存

5. レスポンス返却
   └─ JSON形式でレポートデータを返却
```

### フローチャート

```mermaid
flowchart TD
    A[API呼び出し] --> B{キャッシュ存在?}
    B -->|Yes| C[キャッシュデータを返却]
    B -->|No| D[YearInReviewBuilder初期化]
    D --> E[年間会話数を集計]
    E --> F[最も忙しかった日を特定]
    F --> G[平均応答時間を算出]
    G --> H[ui_settingsにキャッシュ保存]
    H --> I[JSON レスポンス返却]
    C --> I
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-120-01 | デフォルト年度 | yearが指定されない場合は2025年 | year未指定時 |
| BR-120-02 | ユーザー固有データ | Current.userに割り当てられた会話のみ対象 | 常時 |
| BR-120-03 | キャッシュ利用 | 同一年度のデータはui_settingsにキャッシュ | 常時 |
| BR-120-04 | 日付範囲 | 対象年の1月1日〜12月31日 | 常時 |

### 計算ロジック

**年間会話数の計算**：
```ruby
account.conversations.where(assignee_id: user_id, created_at: year_range).count
```

**最も忙しかった日の特定**：
```ruby
daily_counts = account.conversations
                      .where(assignee_id: user_id, created_at: year_range)
                      .group_by_day(:created_at, range: year_range)
                      .count
daily_counts.max_by { |_date, count| count }
```

**平均応答時間の算出**：
```ruby
account.reporting_events
       .where(name: 'first_response', user_id: user_id, created_at: year_range)
       .average(:value)
```

## データベース操作仕様

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 会話数集計 | conversations | SELECT | 年間の会話数をカウント |
| 日別集計 | conversations | SELECT | 日別会話数をグループ化してカウント |
| 応答時間集計 | reporting_events | SELECT | 平均応答時間を算出 |
| キャッシュ読取 | users | SELECT | ui_settingsからキャッシュを読取 |
| キャッシュ保存 | users | UPDATE | ui_settingsにキャッシュを保存 |

### テーブル別操作詳細

#### conversations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | COUNT(*) | assignee_id = user_id, created_at BETWEEN year_start AND year_end | 年間会話数 |
| SELECT | COUNT(*) GROUP BY DATE(created_at) | 同上 | 日別会話数 |

#### reporting_events

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | AVG(value) | name = 'first_response', user_id = user_id, created_at BETWEEN year_start AND year_end | 平均応答時間 |

#### users

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | ui_settings | キャッシュキー: year_in_review_{account_id}_{year} | キャッシュ保存 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 401 | Unauthorized | 未ログインの場合 | ログインして再リクエスト |

### リトライ仕様

該当なし（同期処理のため）

## トランザクション仕様

読み取り処理が主だが、キャッシュ保存時にユーザーレコードを更新する。

## パフォーマンス要件

- 年間データの集計でも迅速にレスポンスを返すこと
- キャッシュ利用により2回目以降は高速にレスポンス

## セキュリティ考慮事項

- 自身のデータのみ参照可能（Current.userでスコープ）
- アカウント間のデータ分離はCurrent.accountスコープで担保

## 備考

- フロントエンドでは「Year in Review」バナーやモーダルでこのデータを表示
- キャッシュはui_settingsに保存されるため、ユーザー設定画面からクリア可能
- busiest_dayがnullの場合は会話がなかったことを示す

---

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

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

### 推奨読解順序

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

まず、出力されるデータの構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | year_in_review_builder.rb | `app/builders/year_in_review_builder.rb` | buildメソッドの返却値構造 |

**読解のコツ**: 出力は{year, total_conversations, busiest_day, support_personality}の構造。busiest_dayは{date, count}を含み、support_personalityは{avg_response_time_seconds}を含む。

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

APIコントローラーからの処理フローを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | year_in_reviews_controller.rb | `app/controllers/api/v2/accounts/year_in_reviews_controller.rb` | showアクション |

**主要処理フロー**:
1. **2-4行目**: yearパラメータの取得とデフォルト値設定
2. **5-6行目**: キャッシュキーの生成とキャッシュチェック
3. **8-9行目**: キャッシュヒット時はそのまま返却
4. **11-16行目**: キャッシュミス時はYearInReviewBuilderでデータ生成
5. **18-22行目**: ui_settingsにキャッシュ保存
6. **24行目**: JSONレスポンス返却

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

実際のデータ集計ロジックを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | year_in_review_builder.rb | `app/builders/year_in_review_builder.rb` | 年間データ集計ロジック |

**主要処理フロー**:
- **10-16行目**: buildメソッドで各メトリクスを集計して返却
- **20-26行目**: year_rangeで対象年の範囲を計算
- **28-33行目**: total_conversations_countで年間会話数を取得
- **35-51行目**: busiest_day_dataで最も忙しかった日を特定
- **53-61行目**: support_personality_dataで平均応答時間を算出
- **63-73行目**: average_response_timeでReportingEventから平均値を取得

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

```
YearInReviewsController#show
    │
    ├─ params[:year] || 2025
    │
    ├─ cache_key 生成
    │      └─ "year_in_review_#{account_id}_#{year}"
    │
    ├─ Current.user.ui_settings.dig(cache_key)
    │      │
    │      ├─ (cache hit)
    │      │      └─ render json: cached_data
    │      │
    │      └─ (cache miss)
    │             │
    │             └─ YearInReviewBuilder#new
    │                    │
    │                    └─ YearInReviewBuilder#build
    │                           │
    │                           ├─ year_range
    │                           │      └─ Jan 1 - Dec 31
    │                           │
    │                           ├─ total_conversations_count
    │                           │      └─ conversations.where(assignee_id, created_at).count
    │                           │
    │                           ├─ busiest_day_data
    │                           │      └─ conversations.group_by_day.max_by
    │                           │
    │                           └─ support_personality_data
    │                                  └─ reporting_events.where(first_response).average(:value)
    │
    └─ Current.user.update(ui_settings: {..., cache_key => data})
```

### データフロー図

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

year パラメータ ──────────────▶ year_range 計算 ─────────────────┐
                                │                                 │
Current.user ────────────────▶ ui_settings キャッシュチェック ───┤
                                │                                 │
Current.account ─────────────▶ YearInReviewBuilder               │
                                │                                 │
                              ┌─┴─────────────────────────────────┘
                              │
                              ▼
                         キャッシュミス時:
                              │
                              ├─▶ conversations.where(assignee_id, created_at)
                              │      .count
                              │      → total_conversations
                              │
                              ├─▶ conversations.group_by_day.max_by
                              │      → busiest_day { date, count }
                              │
                              └─▶ reporting_events.where(first_response)
                                   .average(:value)
                                   → support_personality { avg_response_time_seconds }
                                        │
                                        ▼
                              ui_settings にキャッシュ保存
                                        │
                                        ▼
                         { year, total_conversations,
                           busiest_day, support_personality }
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| year_in_reviews_controller.rb | `app/controllers/api/v2/accounts/year_in_reviews_controller.rb` | コントローラー | APIエンドポイント |
| year_in_review_builder.rb | `app/builders/year_in_review_builder.rb` | ビルダー | 年間データ集計 |
| reporting_event.rb | `app/models/reporting_event.rb` | モデル | 応答時間データ |
| conversation.rb | `app/models/conversation.rb` | モデル | 会話データ |
| user.rb | `app/models/user.rb` | モデル | ユーザー（ui_settings） |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義（449行目） |
| YearInReviewBanner.vue | `app/javascript/dashboard/components-next/year-in-review/YearInReviewBanner.vue` | フロントエンド | バナー表示 |
| YearInReviewModal.vue | `app/javascript/dashboard/components-next/year-in-review/YearInReviewModal.vue` | フロントエンド | モーダル表示 |
