# 画面設計書 3-検索画面

## 概要

本ドキュメントは、InvenTree Flutterアプリケーションの検索画面（SearchWidget）の画面設計を定義するものである。

### 本画面の処理概要

検索画面はデータベース全体を横断検索する機能を提供し、部品、在庫、注文、会社など様々なデータを一括で検索できる。

**業務上の目的・背景**：在庫管理業務において、特定のアイテムを迅速に見つけ出すことは効率的な業務遂行に不可欠である。本画面は単一の検索ボックスから全データベースを横断的に検索し、該当するアイテムを素早く特定できる。これにより、部品番号、在庫アイテム、発注番号など様々な情報へのアクセス時間を大幅に短縮する。

**画面へのアクセス方法**：BottomAppBarの検索アイコンをタップ、またはホーム画面等から検索機能を呼び出す。

**主要な操作・処理内容**：
1. 検索クエリの入力（TextFormFieldへのテキスト入力）
2. デバウンス付き自動検索の実行（300ms遅延）
3. 統合検索API（API v102以上）またはレガシー検索の使い分け
4. 検索結果のカテゴリ別件数表示
5. 検索結果カテゴリ押下で該当一覧画面へ遷移
6. 検索クエリのクリア

**画面遷移**：
- 遷移元：BottomAppBar（任意の画面）
- 遷移先：部品一覧画面、部品カテゴリ一覧画面、在庫アイテム一覧画面、在庫ロケーション一覧画面、購入注文一覧画面、販売注文一覧画面、会社一覧画面、サプライヤー部品一覧画面

**権限による表示制御**：各検索カテゴリはユーザーの権限（canView）に応じて検索対象に含まれる/含まれないが制御される。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 49 | グローバル検索 | 主機能 | 検索クエリ入力と全データベース横断検索の実行 |
| 50 | 統合検索API | API連携 | サーバーの統合検索APIを使用した効率的な検索処理 |
| 4 | パーツ一覧表示 | 遷移先機能 | パーツ検索結果押下時にパーツ一覧画面へ遷移 |
| 14 | 在庫アイテム一覧表示 | 遷移先機能 | 在庫検索結果押下時に在庫一覧画面へ遷移 |
| 23 | 発注一覧表示 | 遷移先機能 | 発注検索結果押下時に発注一覧画面へ遷移 |
| 32 | 受注一覧表示 | 遷移先機能 | 受注検索結果押下時に受注一覧画面へ遷移 |
| 38 | 会社一覧表示 | 遷移先機能 | 会社検索結果押下時に会社一覧画面へ遷移 |

## 画面種別

検索画面（全文検索型）

## URL/ルーティング

- Flutter Widget：`SearchWidget`（StatefulWidget）
- 状態クラス：`_SearchDisplayState`（RefreshableStateを継承）

## 入出力項目

| 項目名 | 入力/出力 | データ型 | 必須 | 説明 |
|--------|----------|----------|------|------|
| hasAppbar | 入力 | bool | 必須 | AppBarを表示するかどうかのフラグ |
| searchController.text | 入力 | String | - | 検索クエリ文字列 |

## 表示項目

| 項目名 | データ型 | 説明 | 取得元 |
|--------|----------|------|--------|
| 検索入力フィールド | TextFormField | 検索クエリ入力欄 | ユーザー入力 |
| クリア/検索アイコン | Icon | 入力状態に応じたアイコン（空=検索、入力済=クリア） | searchController.text |
| 検索中インジケーター | Widget | 検索実行中を示すプログレス表示 | isSearching() |
| 部品検索結果 | ListTile | 部品の検索結果件数 | nPartResults |
| カテゴリ検索結果 | ListTile | 部品カテゴリの検索結果件数 | nCategoryResults |
| 在庫アイテム検索結果 | ListTile | 在庫アイテムの検索結果件数 | nStockResults |
| 在庫ロケーション検索結果 | ListTile | 在庫ロケーションの検索結果件数 | nLocationResults |
| 購入注文検索結果 | ListTile | 購入注文の検索結果件数 | nPurchaseOrderResults |
| 販売注文検索結果 | ListTile | 販売注文の検索結果件数 | nSalesOrderResults |
| 会社検索結果 | ListTile | 会社の検索結果件数 | nCompanyResults |
| 顧客検索結果 | ListTile | 顧客の検索結果件数 | nCustomerResults |
| 製造業者検索結果 | ListTile | 製造業者の検索結果件数 | nManufacturerResults |
| サプライヤー検索結果 | ListTile | サプライヤーの検索結果件数 | nSupplierResults |
| サプライヤー部品検索結果 | ListTile | サプライヤー部品の検索結果件数 | nSupplierPartResults |
| 検索結果なし | ListTile | 該当データがない場合のメッセージ | results.isEmpty |

## イベント仕様

### 1-検索クエリ入力

検索入力フィールドにテキストを入力した際に自動検索を実行する。

**処理フロー**：
1. ユーザーがTextFormFieldにテキストを入力
2. `onChanged`コールバックで`onSearchTextChanged(text)`が呼び出される
3. 300msのデバウンスタイマーが開始される
4. タイマー完了後に`search(text)`が実行される
5. API v102以上の場合は統合検索API（`search/`エンドポイント）を使用
6. API v102未満の場合は`legacySearch()`で個別にカウントを取得
7. 検索結果を状態変数に保存し、UIを更新

### 2-検索クエリクリア

クリアアイコン（バックスペースアイコン）をタップした際に検索をクリアする。

**処理フロー**：
1. ユーザーがクリアアイコンをタップ
2. `searchController.clear()`で入力をクリア
3. `onSearchTextChanged("", immediate: true)`で即時検索リセット
4. `resetSearchResults()`で全結果カウンタを0にリセット

### 3-部品検索結果押下

部品の検索結果タイルをタップした際に部品一覧画面へ遷移する。

**処理フロー**：
1. ユーザーが部品結果タイルをタップ
2. `Navigator.push()`で`PartList({"original_search": query})`へ遷移
3. 検索クエリがフィルタとして引き継がれる

### 4-在庫アイテム検索結果押下

在庫アイテムの検索結果タイルをタップした際に在庫アイテム一覧画面へ遷移する。

**処理フロー**：
1. ユーザーが在庫アイテム結果タイルをタップ
2. `Navigator.push()`で`StockItemList({"original_search": query})`へ遷移

### 5-購入注文検索結果押下

購入注文の検索結果タイルをタップした際に購入注文一覧画面へ遷移する。

**処理フロー**：
1. ユーザーが購入注文結果タイルをタップ
2. `Navigator.push()`で`PurchaseOrderListWidget(filters: {"original_search": query})`へ遷移

### 6-販売注文検索結果押下

販売注文の検索結果タイルをタップした際に販売注文一覧画面へ遷移する。

**処理フロー**：
1. ユーザーが販売注文結果タイルをタップ
2. `Navigator.push()`で`SalesOrderListWidget(filters: {"original_search": query})`へ遷移

### 7-会社検索結果押下

会社の検索結果タイルをタップした際に会社一覧画面へ遷移する。

**処理フロー**：
1. ユーザーが会社結果タイルをタップ
2. `Navigator.push()`で`CompanyListWidget(L10().companies, {"original_search": query})`へ遷移

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

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| 検索実行 | なし | SELECT（API経由） | サーバーAPIへの検索リクエスト |

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

本画面はデータの参照のみで、データベース更新は行わない。

## メッセージ仕様

| メッセージID | 種別 | メッセージ内容 | 表示条件 |
|-------------|------|--------------|---------|
| MSG-001 | プレースホルダ | L10().queryEmpty | 検索入力欄が空の時のヒントテキスト |
| MSG-002 | 情報 | L10().searching | 検索実行中 |
| MSG-003 | 情報 | L10().queryNoResults | 検索結果が0件の時 |
| MSG-004 | 表示 | L10().parts | 部品検索結果のタイトル |
| MSG-005 | 表示 | L10().partCategories | 部品カテゴリ検索結果のタイトル |
| MSG-006 | 表示 | L10().stockItems | 在庫アイテム検索結果のタイトル |
| MSG-007 | 表示 | L10().stockLocations | 在庫ロケーション検索結果のタイトル |
| MSG-008 | 表示 | L10().purchaseOrders | 購入注文検索結果のタイトル |
| MSG-009 | 表示 | L10().salesOrders | 販売注文検索結果のタイトル |
| MSG-010 | 表示 | L10().companies | 会社検索結果のタイトル |
| MSG-011 | 表示 | L10().customers | 顧客検索結果のタイトル |
| MSG-012 | 表示 | L10().manufacturers | 製造業者検索結果のタイトル |
| MSG-013 | 表示 | L10().suppliers | サプライヤー検索結果のタイトル |
| MSG-014 | 表示 | L10().supplierParts | サプライヤー部品検索結果のタイトル |

## 例外処理

| 例外種別 | 発生条件 | 対応処理 |
|---------|---------|---------|
| 検索クエリが空 | searchController.text.isEmpty | 検索をスキップし、結果をリセット |
| APIレスポンスが無効 | response.isValid()がfalse | resetSearchResults()で結果をリセット |
| 検索キャンセル | 新しい検索開始時に前回の検索が未完了 | CancelableOperation.cancel()で前回検索をキャンセル |

## 備考

- 検索はデバウンス（300ms）付きで自動実行される
- API v102以上の場合は統合検索APIを使用し、1回のリクエストで全カテゴリの結果を取得
- API v102未満の場合はレガシー検索（個別カウントAPI）を使用
- 検索クエリは遷移先画面に`original_search`パラメータとして引き継がれる
- API v315以上ではサプライヤー/顧客/製造業者の分割検索をサポート

---

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

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

### 推奨読解順序

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

まず、検索結果の保持方法を理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | search.dart | `lib/widget/search.dart` | 検索結果を保持する状態変数（nPartResults, nStockResults等）を理解する |

**読解のコツ**: 検索結果は各カテゴリごとにint型のカウンタ変数として保持される。これらは`resetSearchResults()`で一括リセットされる。

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

処理の起点となるファイル・関数を特定する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | search.dart | `lib/widget/search.dart` | SearchWidgetと_SearchDisplayStateの構造を理解する |

**主要処理フロー**:
1. **行27-34**: SearchWidgetクラスのコンストラクタでhasAppbarフラグを受け取る
2. **行62**: TextEditingController searchControllerの定義
3. **行125-137**: `onSearchTextChanged()`でデバウンス付き検索を開始
4. **行236-295**: `search()`でAPIバージョンに応じた検索を実行
5. **行165-231**: `_perform_search()`で統合検索APIの結果を処理
6. **行300-377**: `legacySearch()`でレガシー検索を実行
7. **行379-670**: `getTiles()`で検索UIと結果一覧を構築

#### Step 3: API連携を理解する

サーバーとの通信を担当する部分を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | api.dart | `lib/api.dart` | supportsConsolidatedSearch（行291）、supportsSplitCompanySearch（行351）を理解する |

**主要処理フロー**:
- **行291**: `supportsConsolidatedSearch`でAPI v102以上を判定
- **行351**: `supportsSplitCompanySearch`でAPI v315以上を判定

#### Step 4: 画面遷移先を理解する

検索結果から遷移する各画面のWidgetを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | part_list.dart | `lib/widget/part/part_list.dart` | 部品一覧画面の構造 |
| 4-2 | stock_list.dart | `lib/widget/stock/stock_list.dart` | 在庫アイテム一覧画面の構造 |
| 4-3 | category_list.dart | `lib/widget/part/category_list.dart` | 部品カテゴリ一覧画面の構造 |
| 4-4 | location_list.dart | `lib/widget/stock/location_list.dart` | 在庫ロケーション一覧画面の構造 |

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

```
SearchWidget (search.dart)
    |
    +-- _SearchDisplayState
            |
            +-- getTiles()
            |       |
            |       +-- TextFormField (検索入力)
            |       |       +-- onChanged: onSearchTextChanged()
            |       |
            |       +-- GestureDetector (クリアボタン)
            |       |       +-- searchController.clear()
            |       |       +-- onSearchTextChanged("", immediate: true)
            |       |
            |       +-- ListTile x N (検索結果)
            |               +-- Navigator.push()
            |
            +-- onSearchTextChanged()
            |       |
            |       +-- debounceTimer (300ms)
            |               +-- search()
            |
            +-- search()
                    |
                    +-- [API >= 102] _perform_search()
                    |       +-- InvenTreeAPI().post("search/", ...)
                    |       +-- getSearchResultCount()
                    |
                    +-- [API < 102] legacySearch()
                            +-- InvenTreePart().count()
                            +-- InvenTreePartCategory().count()
                            +-- InvenTreeStockItem().count()
                            +-- InvenTreeStockLocation().count()
                            +-- InvenTreePurchaseOrder().count()
```

### データフロー図

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

検索クエリ             -->  onSearchTextChanged()           -->  debounceTimer開始
                                |
                                v (300ms後)
                        search()                            -->  API選択
                                |
        +-------------------+---+---+
        |                           |
        v                           v
[API >= 102]              [API < 102]
_perform_search()         legacySearch()
        |                           |
        v                           v
POST "search/"            各Model.count() x N
        |                           |
        v                           v
getSearchResultCount()    個別カウンタ更新
        |                           |
        +-------------+-------------+
                      |
                      v
              setState()                                    -->  UI更新
                      |
                      v
              getTiles()                                    -->  結果リスト表示
                      |
                      v
              [タイル選択]                                   -->  Navigator.push()
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| search.dart | `lib/widget/search.dart` | ソース | 検索画面のメインWidget |
| api.dart | `lib/api.dart` | ソース | API通信・バージョン判定 |
| refreshable_state.dart | `lib/widget/refreshable_state.dart` | ソース | RefreshableState基底クラス |
| l10.dart | `lib/l10.dart` | ソース | 多言語対応 |
| part.dart | `lib/inventree/part.dart` | ソース | InvenTreePart/InvenTreePartCategoryモデル |
| stock.dart | `lib/inventree/stock.dart` | ソース | InvenTreeStockItem/InvenTreeStockLocationモデル |
| purchase_order.dart | `lib/inventree/purchase_order.dart` | ソース | InvenTreePurchaseOrderモデル |
| sales_order.dart | `lib/inventree/sales_order.dart` | ソース | InvenTreeSalesOrderモデル |
| company.dart | `lib/inventree/company.dart` | ソース | InvenTreeCompany/InvenTreeSupplierPartモデル |
| part_list.dart | `lib/widget/part/part_list.dart` | ソース | 部品一覧画面（遷移先） |
| stock_list.dart | `lib/widget/stock/stock_list.dart` | ソース | 在庫一覧画面（遷移先） |
| category_list.dart | `lib/widget/part/category_list.dart` | ソース | カテゴリ一覧画面（遷移先） |
| location_list.dart | `lib/widget/stock/location_list.dart` | ソース | ロケーション一覧画面（遷移先） |
| purchase_order_list.dart | `lib/widget/order/purchase_order_list.dart` | ソース | 購入注文一覧画面（遷移先） |
| sales_order_list.dart | `lib/widget/order/sales_order_list.dart` | ソース | 販売注文一覧画面（遷移先） |
| company_list.dart | `lib/widget/company/company_list.dart` | ソース | 会社一覧画面（遷移先） |
| supplier_part_list.dart | `lib/widget/company/supplier_part_list.dart` | ソース | サプライヤー部品一覧画面（遷移先） |
