# 画面設計書 16-メンバー管理

## 概要

本ドキュメントは、OpenCodeコンソールのメンバー管理画面の設計書である。ワークスペースのメンバー招待、ロール変更、月間利用上限の設定、メンバー削除を行う管理画面である。

### 本画面の処理概要

メンバー管理画面は、ワークスペースに所属するユーザーを管理するための画面である。管理者はこの画面で新しいメンバーを招待し、既存メンバーのロール（管理者/メンバー）を変更し、月間利用上限を設定し、メンバーを削除することができる。一般メンバーはメンバー一覧を閲覧することはできるが、招待・編集・削除の操作は行えない。

**業務上の目的・背景**：チームでOpenCode Zenを利用する際、各メンバーの権限管理と利用量の制御が必要となる。本画面はワークスペース管理者がチームメンバーを効率的に管理し、コスト管理のために月間利用上限を設定できるようにする。

**画面へのアクセス方法**：ワークスペース詳細レイアウトのナビゲーションから「Members」を選択、または`/workspace/:id/members`に直接アクセス。

**主要な操作・処理内容**：
1. メンバー一覧の表示（メール、ロール、月間使用量/上限、招待状態）
2. 新規メンバーの招待（メール、ロール、月間上限を指定）
3. 既存メンバーのロール・月間上限の編集
4. メンバーの削除（自分自身は削除不可）

**画面遷移**：本画面内で完結する操作が主。ベータ機能説明へのリンクあり（/docs/zen/#for-teams）。

**権限による表示制御**：管理者のみが招待・編集・削除のアクション列を表示。一般メンバーは閲覧のみ可能。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 28 | 認証管理 | 主機能 | メンバー招待・ロール変更の認証管理 |

## 画面種別

一覧 / 登録 / 編集 / 削除

## URL/ルーティング

| 環境 | URL |
|------|-----|
| 本番 | `https://opencode.ai/workspace/:id/members` |
| 開発 | `http://localhost:3000/workspace/:id/members` |

ルーティング定義: `packages/console/app/src/routes/workspace/[id]/members/index.tsx`

## 入出力項目

### 入力項目（メンバー招待）

| 項目名 | 項目ID | データ型 | 必須 | 入力形式 | バリデーション | 備考 |
|--------|--------|----------|------|----------|----------------|------|
| メールアドレス | email | string | Yes | テキスト入力 | 必須チェック、空白トリム | 招待先のメールアドレス |
| ロール | role | string | Yes | ドロップダウン | 必須チェック | "admin" または "member" |
| 月間利用上限 | limit | number | No | 数値入力 | 0以上の整数、null許可 | 空欄は無制限 |
| ワークスペースID | workspaceID | string | Yes | hidden | - | URLパラメータから設定 |

### 入力項目（メンバー編集）

| 項目名 | 項目ID | データ型 | 必須 | 入力形式 | バリデーション | 備考 |
|--------|--------|----------|------|----------|----------------|------|
| メンバーID | id | string | Yes | hidden | 必須チェック | 編集対象のユーザーID |
| ロール | role | string | Yes | ドロップダウン | 必須チェック | 自分自身は変更不可 |
| 月間利用上限 | limit | number | No | 数値入力 | 0以上の整数、null許可 | 空欄は無制限 |

### 出力項目（メンバー一覧）

| 項目名 | データ型 | 表示条件 | 備考 |
|--------|----------|----------|------|
| メールアドレス | string | 常時 | authEmail または email |
| ロール | string | 常時 | "Admin" または "Member" |
| 月間使用量/上限 | string | 常時 | "$X.XX / $Y" または "$X.XX / no limit" |
| 招待状態 | string | timeSeen未設定時 | "Invited" |

## 表示項目

### セクションタイトル

| 要素 | 内容 |
|------|------|
| タイトル | workspace.members.title |
| サブタイトル | workspace.members.subtitle |
| 招待ボタン | workspace.members.invite（管理者のみ） |

### ベータ通知

| 要素 | 内容 |
|------|------|
| テキスト | workspace.members.beta.beforeLink |
| リンク | common.learnMore → /docs/zen/#for-teams |

### 招待フォーム（条件付き表示: store.show=true）

| 要素 | コンポーネント | 説明 |
|------|---------------|------|
| 招待先メール | input[type=text] | フォーカス自動設定 |
| ロール選択 | RoleDropdown | "Admin" / "Member" |
| 月間上限 | input[type=number] | min=0、プレースホルダー="No Limit" |
| エラーメッセージ | div | submission.result.error時に表示 |
| キャンセルボタン | button | フォームを閉じる |
| 招待ボタン | button | メンバー招待を実行 |

### メンバー一覧テーブル

| カラム | キー | 表示条件 | 説明 |
|--------|------|----------|------|
| Email | workspace.members.table.email | 常時 | メンバーのメールアドレス |
| Role | workspace.members.table.role | 常時 | ロール（ドロップダウンまたはテキスト） |
| Monthly Limit | workspace.members.table.monthLimit | 常時 | 使用量/上限（入力または表示） |
| Status | - | 常時 | 招待状態または空 |
| Actions | - | 管理者のみ | 編集/保存/キャンセル/削除ボタン |

## イベント仕様

### 1-招待ボタンクリック（フォーム表示）

| 項目 | 内容 |
|------|------|
| トリガー | 「Invite」ボタンクリック（管理者のみ） |
| 処理 | show()関数が実行される |
| 詳細フロー | 1. submission.clear()で前回結果をクリア<br>2. setStore({ show: true, selectedRole: "member", limit: "" })<br>3. setTimeout()で入力フィールドにフォーカス |

### 2-メンバー招待（フォーム送信）

| 項目 | 内容 |
|------|------|
| トリガー | 招待フォームの「Invite」ボタンクリック |
| 処理 | inviteMember actionが実行される |
| 詳細フロー | 1. [SERVER] email取得、空白トリム、必須チェック<br>2. [SERVER] role取得、必須チェック<br>3. [SERVER] monthlyLimit取得、null許可、負数チェック<br>4. [SERVER] User.invite()でメンバー招待<br>5. 成功時: フォームを閉じ、一覧を再取得<br>6. 失敗時: エラーメッセージを表示 |

### 3-メンバー編集（インライン編集）

| 項目 | 内容 |
|------|------|
| トリガー | 一覧の「Edit」ボタンクリック |
| 処理 | MemberRowのshow()が実行される |
| 詳細フロー | 1. setStore({ editing: true, selectedRole: 現在のロール, limit: 現在の上限 })<br>2. ロールをRoleDropdownで編集可能に<br>3. 月間上限を入力フィールドで編集可能に |

### 4-メンバー更新（保存）

| 項目 | 内容 |
|------|------|
| トリガー | 「Save」ボタンクリック |
| 処理 | updateMember actionが実行される |
| 詳細フロー | 1. [SERVER] id、role、monthlyLimitを取得<br>2. [SERVER] User.update()でメンバー情報更新<br>3. 成功時: 編集モード終了、一覧を再取得<br>4. 失敗時: エラーメッセージを表示 |

### 5-メンバー削除

| 項目 | 内容 |
|------|------|
| トリガー | 「Delete」ボタンクリック（自分以外） |
| 処理 | removeMember actionが実行される |
| 詳細フロー | 1. [SERVER] id取得、必須チェック<br>2. [SERVER] User.remove()でメンバー削除<br>3. 一覧を再取得（revalidate: listMembers.key） |

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

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| ページロード | UserTable | SELECT | メンバー一覧の取得 |
| メンバー招待 | UserTable | INSERT | 新規メンバーの登録 |
| メンバー更新 | UserTable | UPDATE | ロール・月間上限の変更 |
| メンバー削除 | UserTable | DELETE | メンバーの削除 |

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

#### listMembers クエリ

```typescript
const listMembers = query(async (workspaceID: string) => {
  return withActor(async () => {
    return {
      members: await User.list(),
      actorID: Actor.userID(),
      actorRole: Actor.userRole(),
    }
  }, workspaceID)
}, "member.list")
```

#### inviteMember アクション

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|--------|------|
| INSERT | email | フォーム入力値 | 空白トリム済み |
| INSERT | role | "admin" または "member" | ドロップダウン選択 |
| INSERT | monthlyLimit | 数値 または null | null=無制限 |

#### updateMember アクション

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|--------|------|
| UPDATE | role | "admin" または "member" | 自分自身は変更不可 |
| UPDATE | monthlyLimit | 数値 または null | null=無制限 |

#### removeMember アクション

| 操作 | 項目 | 削除条件 | 備考 |
|-----|------|---------|------|
| DELETE | - | id一致 | 自分自身は削除不可 |

## メッセージ仕様

| メッセージID | メッセージ種別 | 表示条件 | メッセージ内容（キー） |
|-------------|---------------|----------|----------------------|
| title | タイトル | 常時 | workspace.members.title |
| subtitle | サブタイトル | 常時 | workspace.members.subtitle |
| invite | ボタン | 管理者 | workspace.members.invite |
| inviting | ボタン | 招待中 | workspace.members.inviting |
| edit | ボタン | 管理者 | workspace.members.edit |
| save | ボタン | 編集中 | workspace.members.save |
| saving | ボタン | 保存中 | workspace.members.saving |
| delete | ボタン | 管理者 | workspace.members.delete |
| invited | 状態 | timeSeen未設定 | workspace.members.invited |
| noLimit | プレースホルダー | フォーム | workspace.members.noLimit |
| noLimitLowercase | 表示 | 上限なし | workspace.members.noLimitLowercase |
| role.admin | ロール | 常時 | workspace.members.role.admin |
| role.member | ロール | 常時 | workspace.members.role.member |

## 例外処理

| 例外パターン | 検出方法 | 対応処理 | ユーザーへの通知 |
|------------|----------|---------|----------------|
| メール未入力 | サーバーサイドバリデーション | { error: formError.emailRequired } | フォーム内エラー表示 |
| ロール未選択 | サーバーサイドバリデーション | { error: formError.roleRequired } | フォーム内エラー表示 |
| 月間上限負数 | サーバーサイドバリデーション | { error: formError.monthlyLimitInvalid } | フォーム内エラー表示 |
| 招待エラー | User.invite()のcatch | { error: e.message } | フォーム内エラー表示 |
| 更新エラー | User.update()のcatch | { error: e.message } | インラインエラー表示 |
| 削除エラー | User.remove()のcatch | { error: e.message } | （通常発生しない） |

## 備考

- 使用量は`monthlyUsage / 100000000`で$単位に変換
- 当月以外の使用量は0にリセット（timeMonthlyUsageUpdatedでチェック）
- RoleDropdownコンポーネントでロール選択UI提供
- 自分自身のロールは編集不可（isCurrentUser判定）
- 自分自身は削除不可（isCurrentUser判定）
- CSSモジュール（member-section.module.css）でスタイリング

---

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

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

### 推奨読解順序

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

本コンポーネントでは4つのサーバーサイド関数を使用する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | member-section.tsx | `packages/console/app/src/routes/workspace/[id]/members/member-section.tsx` | listMembers（14-23行目）、inviteMember（25-46行目）、removeMember（48-64行目）、updateMember（66-89行目） |
| 1-2 | user.sql.js | `packages/console-core/src/schema/user.sql.js` | UserRole定義 |

**読解のコツ**: actorID、actorRoleを一覧クエリで取得し、現在のユーザーの操作権限と自分自身かどうかの判定に使用。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | index.tsx | `packages/console/app/src/routes/workspace/[id]/members/index.tsx` | MemberSectionコンポーネントの配置 |
| 2-2 | member-section.tsx | `packages/console/app/src/routes/workspace/[id]/members/member-section.tsx` | MemberSection関数（219-368行目）、MemberRow関数（91-217行目） |

**主要処理フロー**:
1. **220行目**: useParams()でワークスペースID取得
2. **223行目**: createAsync()でメンバー一覧取得
3. **224行目**: useSubmission()で招待アクションの状態監視
4. **225-229行目**: createStore()でフォーム表示・選択状態管理
5. **233-244行目**: roleOptionsの定義（admin/member）
6. **246-250行目**: createEffect()で招待成功時にフォームを閉じる
7. **287-335行目**: 招待フォームのレンダリング
8. **336-365行目**: メンバー一覧テーブルのレンダリング

#### Step 3: MemberRowコンポーネントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | member-section.tsx | `packages/console/app/src/routes/workspace/[id]/members/member-section.tsx` | MemberRow関数（91-217行目） |

**読解のコツ**: 各行のインライン編集状態（store.editing）、ロール/上限の変更UI、自分自身の判定（isCurrentUser）を確認。

#### Step 4: RoleDropdownコンポーネントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | role-dropdown.tsx | `packages/console/app/src/routes/workspace/[id]/members/role-dropdown.tsx` | ロール選択ドロップダウンUI |

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

```
MembersPage (index.tsx)
    |
    +-- <MemberSection />

MemberSection (member-section.tsx)
    |
    +-- useParams() -> params.id取得
    |
    +-- createAsync(listMembers)
    |       +-- [SERVER] withActor()
    |               +-- User.list() -> メンバー一覧
    |               +-- Actor.userID() -> 現在のユーザーID
    |               +-- Actor.userRole() -> 現在のロール
    |
    +-- useSubmission(inviteMember) -> submission状態
    |
    +-- createStore() -> { show, selectedRole, limit }
    |
    +-- show() -> 招待フォーム表示
    |
    +-- hide() -> 招待フォーム非表示
    |
    +-- <form action={inviteMember}>
    |       +-- <RoleDropdown />
    |       +-- [SERVER] User.invite({ email, role, monthlyLimit })
    |
    +-- <For each={data().members}>
            +-- <MemberRow />
                    |
                    +-- useSubmission(updateMember)
                    +-- createStore() -> { editing, selectedRole, limit }
                    +-- getUsageDisplay() -> 使用量計算
                    +-- <RoleDropdown /> (編集時)
                    +-- <form action={updateMember}> (編集保存)
                    +-- <form action={removeMember}> (削除)
```

### データフロー図

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

ページロード ─────────> listMembers(workspaceID)
                              |
                              v
                        [SERVER] User.list()
                              |
                              v
                        メンバー一覧 + actorInfo ────> テーブル表示
                              |
                              v
                        isAdmin判定 ────────────────> Invite/Edit/Delete表示

招待フォーム ─────────> inviteMember action
(email,role,limit)            |
                              v
                        [SERVER] User.invite()
                              |
                              v
                        revalidate listMembers ─────> テーブル再描画

インライン編集 ────────> updateMember action
(id,role,limit)               |
                              v
                        [SERVER] User.update()
                              |
                              v
                        revalidate listMembers ─────> テーブル再描画

削除ボタン ───────────> removeMember action
                              |
                              v
                        [SERVER] User.remove()
                              |
                              v
                        revalidate listMembers ─────> テーブル再描画
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.tsx | `packages/console/app/src/routes/workspace/[id]/members/index.tsx` | ソース | ページエントリーポイント |
| member-section.tsx | `packages/console/app/src/routes/workspace/[id]/members/member-section.tsx` | ソース | メインコンポーネント |
| member-section.module.css | `packages/console/app/src/routes/workspace/[id]/members/member-section.module.css` | スタイル | セクション固有スタイル |
| role-dropdown.tsx | `packages/console/app/src/routes/workspace/[id]/members/role-dropdown.tsx` | ソース | ロール選択ドロップダウン |
| form-error.ts | `packages/console/app/src/lib/form-error.ts` | ソース | エラーメッセージ定義、localizeError |
| user.js | `packages/console-core/src/user.js` | ソース | Userドメインロジック |
| user.sql.js | `packages/console-core/src/schema/user.sql.js` | ソース | UserRoleスキーマ定義 |
| actor.js | `packages/console-core/src/actor.js` | ソース | アクター管理 |
