チャット・通知
位置づけ: 後続フェーズ
チャット・通知基盤は、営業・事務・施工店・代理店の連絡を CRM 上のチャットに一元化し、社内向けには Slack 通知を併用するハイブリッド構成である。会話は案件(Deal)に紐づくルームとして存在し、雑談チャンネルは対象外とする「文脈必須」原則をとる。
試作実装(crm_app、React + TS)には案件単位のチャット機能が実装済みである。ルームは相手スコープ(社内/施工店/代理店)で固定分割され、案件詳細画面のチャットタブおよび受信箱型の一覧ページから閲覧する。Slack通知は、設定画面のイベント×宛先マトリクスとして定義済みであり、タスク依頼フローでは Incoming Webhook による実送信も動作する。
一方、バックエンド永続化・リアルタイム配信・チャットメッセージ発火のSlack通知配線は未実装である。永続化は現状ブラウザの localStorage(キー crm:chats)に限られ、試作段階のモックデータとして扱う。ロールもログイン主体に紐づく実認証ではなく、サイドバーの「表示ロール(デモ)」で切り替えるクライアント側の値であり、チャット・設定画面はいずれもこの表示ロールを起点に可視範囲を絞り込む設計である。
スコープ/対象
Section titled “スコープ/対象”第1弾(先行リリース: 催事・アイテム(商品マスタ)・商社(卸))には含まれない。本ページはその後続フェーズの仕様である。
出典は3系統に分かれる。混同を避けるため下表で整理する。
| 対象 | 内容 | 実装状況 |
|---|---|---|
| 旧システム向け拡張見積(BT社「チャットシステム拡張開発」) | 顧客ごとのチャットルーム。Pusherでリアルタイム配信 | 未実装(旧システム側の見積案件) |
ChatworkService | 社内ツール ChatWork への通知連携 | 実装済(本件のCRM内チャットとは別物) |
| 新CRM試作の案件チャット | 案件(Deal)×相手スコープでルーム分割 | 実装済(フロント・localStorage 永続化のみ) |
| Slack Incoming Webhook 通知 | タスク依頼から Slack へ送信(通知マトリクスは定義・ON/OFF管理のみで送信には未接続) | 実装済(設定画面から Webhook URL を登録して送信) |
| リアルタイム配信基盤(Pusher/WebSocket) | メッセージのプッシュ配信 | 未実装 |
データ(フィールド表)
Section titled “データ(フィールド表)”会話は案件(Deal)×スコープの組み合わせごとに1ルームとし、1案件につき最大3ルームとする。トピック分けはメッセージ本文側の運用に委ねる。エンティティ定義は lib/chat.ts、通知ルール定義は lib/notifications.ts にある。
ChatThread(ルーム)
Section titled “ChatThread(ルーム)”| フィールド | 型・enum | 必須 | 既定値 | 制約・備考 |
|---|---|---|---|---|
id | string | ✓ | C${Date.now()}-${scope} | ルーム作成時にクライアントが生成する擬似ID。DB連番ではない |
dealId | string | ✓ | — | Deal.id。外部キー制約・存在チェックは未実装 |
scope | RoomScope("社内" | "施工店" | "代理店") | ✓ | — | 1案件×1スコープにつき最大1件。ensureRoom/createRoomsFor が重複作成を防止する |
members | ChatMember[] | ✓ | defaultMembers(scope) | 作成後に第三者を追加する招待APIは未実装。作成時点で固定 |
messages | ChatMessage[] | ✓ | [] | 追記のみ。編集・削除・既読者一覧のAPIは未実装 |
unread | number | ✓ | シードデータのみ任意値、新規作成時は 0 | ルーム単位の単純カウンタ。メッセージ送信時の自動加算は未実装(後述) |
ChatMessage
Section titled “ChatMessage”| フィールド | 型・enum | 必須 | 既定値 | 制約・備考 |
|---|---|---|---|---|
id | string | ✓ | M${Date.now()} | ミリ秒単位のID。理論上は同一ミリ秒内の連続送信でID重複の可能性がある |
author | string | ✓ | CURRENT_USER.name 固定 | 送信APIは常にログイン中ユーザー(自分)名義。他者送信を模擬する経路はUIにない |
role | ChatRole(営業|事務|施工店|代理店) | ✓ | "営業" 固定 | sendMessage() 内の固定値。実際の表示ロールが施工店・代理店等でも、送信メッセージの表示ロールは常に「営業」になる |
text | string | ✓ | — | 送信前に trim()。空文字は送信ボタン非活性化で防止(クライアント側のみ、サーバー検証なし)。文字数上限なし |
at | string(ISO 8601、分単位) | ✓ | new Date().toISOString().slice(0, 16) | 秒は保持しない |
ChatMember
Section titled “ChatMember”| フィールド | 型・enum | 必須 | 既定値 | 制約・備考 |
|---|---|---|---|---|
name | string | ✓ | スコープごとの規定値(自社担当者名/{代理店名} 窓口/施工店名または 施工店(未定)) | defaultMembers() が算出 |
role | ChatRole | ✓ | scope 依存(社内→営業、施工店→施工店、代理店→代理店) | 発言者アイコン・ラベルを出し分ける表示専用の型。権限判定には関与しない |
NotifyRule(通知ルール定義)
Section titled “NotifyRule(通知ルール定義)”| フィールド | 型・enum | 必須 | 既定値 | 制約・備考 |
|---|---|---|---|---|
key | string | ✓ | — | ルール一意キー(24件、重複なし) |
group | string | ✓ | — | 8グループ:架電(前確)/商談/成約・契約後/施工/発注/申請/催事/チャット |
event | string | ✓ | — | 設定画面に表示するイベント名 |
dest | NotifyDest[](9種enum:担当DM/営業DM/#商談前確/#成約/#施工/#発注/#申請/#催事/顧客チャンネル) | ✓ | — | 1ルールに複数宛先を持てる |
timing | string | ✓ | — | 自由記述。cron式などの実行スケジュールではなく、人が読む説明文 |
defaultOn | boolean | ✓ | true(24件すべて) | 設定画面トグルの初期値 |
永続化キー一覧(localStorage)
Section titled “永続化キー一覧(localStorage)”| キー | 内容 | 型 |
|---|---|---|
crm:chats | チャットルーム本体(zustand persist、version: 3) | ChatThread[] |
crm:slack_webhook | Slack Incoming Webhook URL | string |
crm:slack_channel | 通知チャンネル表示名 | string(既定 #crm-bo-requests) |
crm:notify | 通知マトリクスのON/OFF上書き | Record<ruleKey, boolean>(未設定キーは defaultOn を使用) |
| 画面/機能 | 内容 | 実装状況 |
|---|---|---|
案件詳細「チャット」タブ(DealChatTab) | 案件に紐づく最大3ルームをタブ切替で閲覧 | 実装済 |
/chat 受信箱一覧(ChatPage) | 案件を1行に畳み、ルーム・未読・最新メッセージを横断表示 | 実装済 |
ロールベース可視スコープ(visibleScopes()) | 施工店ロールは施工店ルームのみ、代理店ロールは代理店ルームのみ、社内ロールは3スコープすべてを閲覧 | 実装済 |
Slack通知マトリクス(SettingsPage) | イベント×宛先の一覧表示とON/OFF切替 | 実装済 |
| アプリ内通知センター(ベルアイコン・未読一覧) | チャットタブ内バッジ表示と /chat の未読集計で代替 | 未実装 |
モバイル対応の要否・優先度は、催事現場からの利用ニーズに応じて判断する。
DealChatTab の挙動
Section titled “DealChatTab の挙動”- 表示ロールの
visibleScopes()で見えるスコープに絞り、ROOM_SCOPE_ORDER(社内→施工店→代理店)の固定順でタブ表示する。 - ルームが1件もない場合は案内文のみを表示する。内部ロール(社内3スコープが見えるロール)に限り「社内ルームを開始」ボタンを表示する。
- 内部ロールでは、未作成のスコープごとに「+◯◯ルーム」ボタンをタブ列の右端に表示し、
ensureRoom()で即時作成できる。この手動作成は案件ステータスに関わらず常に押下可能で、後述の自動生成条件(成約・工事)とは独立している。 - タブ切替でルームが替わると、
ChatPanel内のuseEffect(thread.id依存)がmarkRead()を呼び、当該ルームのunreadを0にする(コンポーネントの再マウントではなく、thread.idの変化で発火する)。
/chat 受信箱の挙動
Section titled “/chat 受信箱の挙動”visibleDeals(role, deals)とvisibleScopes(role)の両方で二重にフィルタしたうえで、案件(dealId)単位にルームを畳んで1行にする。- 各行の未読数はその案件に属する可視ルームの
unread合計、最新メッセージはルーム横断でatが最大のものを採用する。 - 一覧は最新メッセージの新しい順(降順)で並ぶ。
- 選択案件・選択ルームはURLの検索パラメータ(
sel/room)で管理する。selが未指定または不正な場合は先頭の案件に、roomが未指定または不正な場合は「未読があるルーム→なければ先頭ルーム」にフォールバックする。 - モバイル幅では
selの有無で一覧⇄会話ペインの表示を切り替える(一覧のみ⇄会話のみ)。
設定画面(Slack連携・通知マトリクス)の挙動
Section titled “設定画面(Slack連携・通知マトリクス)の挙動”- Slack連携の「保存」ボタンは、
webhook/channelの入力値が保存済み値と異なる(dirty)ときのみ活性化する。「テスト送信」はWebhook URL未入力時はボタン自体が非活性になる(設定済みならTaskRequestSheetと同様にmode: "no-cors"で実送信する)。 - 通知マトリクスの各行はグループ単位(8グループ)でカード化し、PCでは2カラム表示。スイッチのON/OFF状態は
crm:notifyにルールキー単位で保存され、未保存のルールはdefaultOn(すべてtrue)で表示される。 - 通知マトリクスのON/OFFは実送信に接続していない。 実際にSlackへ送信する経路は
TaskRequestSheet(および設定画面のテスト送信)のみであり、TaskRequestSheetはlib/notifications.tsやcrm:notifyを一切参照しない。したがって設定画面でどのルールをOFFにしても、タスク依頼のSlack送信可否には影響しない(送信可否はWebhook URLの有無のみで決まる)。
業務ルール/ワークフロー
Section titled “業務ルール/ワークフロー”ルーム自動生成
Section titled “ルーム自動生成”案件ステータスが変化した際、useDeals の setStatus / advanceStage から createRoomsForStatus が呼ばれ、useChats.createRoomsFor でルームを作成する。
flowchart TD
A["案件ステータス変更 setStatus / advanceStage"] --> B{"遷移先は?"}
B -->|成約| C["社内ルームを作成対象に追加"]
C --> D{"subjectOf(deal) === 代理店?"}
D -->|Yes| E["代理店ルームも作成対象に追加"]
D -->|No| F["社内ルームのみ"]
B -->|工事| G["施工店ルームを作成対象に追加\nvendor = deal.construction?.vendor ?? 施工店(未定)"]
B -->|その他| H["ルーム作成なし"]
E --> I["createRoomsFor(dealId, scopes)"]
F --> I
G --> I
I --> J{"既存ルームに同一 dealId×scope が存在?"}
J -->|Yes| K["スキップ(重複作成しない)"]
J -->|No| L["新規 ChatThread を先頭に追加\nmembers = defaultMembers(scope, opts)"]
ルームの生成有無は「作成済み(空でもよい)/未作成」の二値であり、案件のステータスとは独立して手動作成もできる。
stateDiagram-v2 [*] --> 未作成 未作成 --> 作成済み_空: 自動生成(成約 / 工事)または手動作成(内部ロールのボタン) 作成済み_空 --> 会話中: sendMessage() 会話中 --> 会話中: sendMessage() / markRead() 作成済み_空 --> [*]: ルーム削除・アーカイブは未実装(到達しない) 会話中 --> [*]: ルーム削除・アーカイブは未実装(到達しない)
既定メンバーはスコープに応じて defaultMembers() が自動セットする。社内ルームは営業担当者本人、代理店ルームは営業担当者と代理店窓口、施工店ルームは営業担当者と施工店名を初期メンバーとする。工事遷移の直後は construction.vendor が未設定(defaultConstruction() は vendor を持たない)であることが多く、その場合は「施工店(未定)」で作成される。
第三者を既存ルームへ追加招待する機能は未実装である。内部ロールは未作成のルームをボタン一つで作成できるが、これは招待ではなくルーム作成にとどまる。
自動生成のシーケンスは以下のとおり。
sequenceDiagram
participant UI as 画面(案件詳細/一覧)
participant DealsStore as useDeals
participant ChatsStore as useChats
participant LS as localStorage(crm:chats)
UI->>DealsStore: setStatus(dealId, "成約") または advanceStage(dealId)
DealsStore->>DealsStore: withStatus() で closeDate / postContract を初期化
DealsStore->>ChatsStore: createRoomsForStatus(deal, "成約")
alt subjectOf(deal) === "代理店"
ChatsStore->>ChatsStore: createRoomsFor(dealId, {agency, scopes:["社内","代理店"]})
else
ChatsStore->>ChatsStore: createRoomsFor(dealId, {agency, scopes:["社内"]})
end
ChatsStore->>ChatsStore: dealId×scope の重複チェック
ChatsStore->>LS: persist ミドルウェアが自動保存
ChatsStore-->>UI: threads 更新 → DealChatTab / ChatPage 再描画
メッセージ送信・既読
Section titled “メッセージ送信・既読”sequenceDiagram
participant User as ユーザー
participant Panel as ChatPanel
participant Store as useChats
participant LS as localStorage(crm:chats)
User->>Panel: ルームを開く(タブ切替)
Panel->>Store: markRead(threadId)(mount 時の useEffect)
Store->>Store: unread を 0 に更新
Store->>LS: persist で自動保存
User->>Panel: メッセージ入力 → 送信(送信ボタン or Cmd/Ctrl+Enter)
Panel->>Panel: text.trim() が空なら中断
Panel->>Store: sendMessage(threadId, text)
Store->>Store: ChatMessage 生成(author = CURRENT_USER.name, role = "営業" 固定)
Store->>LS: persist で自動保存
Store-->>Panel: messages 更新 → 再描画 → 最下部へ自動スクロール
未読カウンタ(unread)はルーム単位の単純カウンタであり、メッセージ送信時に他メンバー分の未読を加算する処理は実装されていない。シードデータに与えられた初期値がルームを開く(markRead)まで表示され続けるのみで、以降の新着に応じた自動加算経路はない。これは単一ユーザーのモック実装であり、他ユーザー端末へメッセージを反映する仕組み自体が存在しないことと表裏一体である(後述「配信基盤とリアルタイム化」)。
Slack通知連携
Section titled “Slack通知連携”src/lib/notifications.ts に24件の通知ルール(NOTIFY_RULES)が定義されており、SettingsPage の通知マトリクスで宛先・タイミングを一覧・ON/OFF切替する。宛先は3系統に整理する。
- 担当DM — 割当・催促など「その人が動く」通知
- 機能チャンネル —
#商談前確#成約#施工#発注#申請#催事などチームで拾う通知 - 顧客チャンネル — 成約時に案件ごとへ自動作成する
実送信が動作するのはタスク依頼(前確・太陽光調査設計、TaskRequestSheet)と設定画面の「テスト送信」のみで、いずれも設定画面に保存した Incoming Webhook URL へ POST する。Webhook未設定時は「Slack未設定」のトースト表示にフォールバックする。それ以外の通知ルールは、設定UI・ルール定義のレベルにとどまり、バックエンドからの自動発火(cronやイベントトリガー)は行わない。前述のとおり、通知マトリクスのON/OFFはこのタスク依頼フローの送信可否に一切影響しない(参照していないため)。
CRM内チャットのメッセージ送信とSlack通知は未接続である。notifications.ts には chat-message ルールが定義されているが、ChatPanel の送信処理からSlack送信を呼び出すコードはない。
| イベントグループ | 代表イベント | 宛先 | 実装状況 |
|---|---|---|---|
| 架電(前確) | 担当割当・リマインド・結果・不出再架電 | 担当DM/#商談前確/営業DM | ルール定義・設定UIのみ |
| 商談 | 結果入力の催促、保留の再連絡リマインド | 営業DM | ルール定義・設定UIのみ |
| 成約・契約後 | 成約時の顧客チャンネル自動作成、審査結果 | #成約/顧客チャンネル/営業DM | ルール定義・設定UIのみ |
| 施工 | 現地写真格納漏れ、完了報告・完工書督促、入金超過 | #施工/担当DM/営業DM | ルール定義・設定UIのみ |
| 発注 | 納期回答メールの案件紐付け(order-reply) | 担当DM | ルール定義・設定UIのみ |
| 発注 | 欠品・納期遅延アラート(order-stock) | #発注 | ルール定義・設定UIのみ |
| 申請 | 印鑑証明期限、申請期限、連携結果未着 | #申請 | ルール定義・設定UIのみ |
| 催事 | 開始/終了報告未提出、日次ファネル集計、アポ数乖離アラート(登録漏れ通知) | #催事/担当DM/営業DM | ルール定義・設定UIのみ(アポ数乖離アラートは判定・可視化まで実装済み。送信配線は他ルールと同様に未接続。詳細は催事モジュール仕様の「通知」節を参照) |
| タスク依頼(前確・太陽光調査設計) | 依頼→担当割振→通知→結果/成果物 | 担当DM | 実送信まで実装済(TaskRequestSheet)。前確の割当には call-assign ルールが定義済みだが TaskRequestSheet はこれを参照せず、太陽光調査設計には対応ルール自体がない(いずれも通知マトリクスとは未接続) |
| チャット | CRMチャット新着(Slack併用期間) | 担当DM | ルール定義のみ(送信処理は未配線) |
タスク依頼フローの業務ルール(TaskRequestSheet)
Section titled “タスク依頼フローの業務ルール(TaskRequestSheet)”- 担当候補は
staffCandidates(type, liveDeals)が算出する。type(前確/太陽光調査設計)に対応するBORole(前確/設計)を持つBO_STAFFを抽出し、各候補のopenCount(未完タスク数)を「タスクのtypeが一致し、assigneeに候補者の名の姓部分(name.split(" ")[0])を含み、statusが完了・NG以外」の件数として数える。openCountの昇順(少ない順)でソートする。 - 初期選択(
assigneeId === nullの状態)は先頭候補、すなわち未完タスク最少の担当者になる。選択自体は必ず人が行う運用とし、自動確定はしない。 - 依頼期限(
due)は依頼確定時点の日付に、前確は+3日、太陽光調査設計は+7日を加算した暦日(土日・祝日を考慮しない)で算出する。 - Slackプレビュー本文(
slackMessage())は、種別ラベル・担当者メンション(<@slackId ?? name>)・案件ID・世帯所在地/氏名、前確では連絡先(携帯優先、なければ固定電話、いずれもなければ「—」)、太陽光調査設計では築年数・面積(いずれも欠損時は「—」/「年不明」)、案件詳細へのCRMリンク(https://crm.valueecology.jp/deals/{dealId})を含む。 - 送信時、Webhook URLが未設定なら実送信をスキップし擬似待機(400ms)のうえでタスクを作成し「Slack未設定」のトーストを出す。Webhook URL設定済みなら Slack Incoming Webhook へ
mode: "no-cors"でPOSTしたうえでタスクを作成する。 mode: "no-cors"の制約により、レスポンスは opaque(内容を判定不能)で、Slack側のエラー(無効なWebhook・チャンネル権限なし等のHTTPエラー)は検知できず常に成功したものとして扱われる。fetch自体が例外を投げるケース(不正なURL文字列・ネットワーク不通など)に限りcatchに入り、この場合はタスク作成(addTask)も実行されない(Slack送信とタスク作成が同一トライブロックで直列に結合しているため)。
質問窓口としての運用
Section titled “質問窓口としての運用”部門ごとの質問窓口スレッドを常設する運用は、コード上に該当概念(区分・専用チャンネル等)を持たない構想段階の機能である。実装するにはスレッドにステータス(未回答/解決済等)を持たせるスキーマ拡張が必要になる。解決済みQ&Aをナレッジとして蓄積し、AIエージェントが一次回答する構想は将来構想とし、AIエージェント構想の方針と接続する。
催事・商社タスクとの連携
Section titled “催事・商社タスクとの連携”催事モジュール(催事モジュール仕様)との接続では、通知ルール ev-start-missing(開始報告未提出)・ev-end-missing(終了報告未提出)・ev-daily(日次ファネル集計)が #催事 チャンネル向けに、event-appointment-gap(終了報告アポ数と実登録数の乖離=登録漏れ通知)が #催事・営業DM 向けに、それぞれ定義済みである。event-appointment-gap は判定源(appointmentGap/shiftsWithAppointmentGap、lib/events.ts の純関数)と可視化(催事シフト詳細のバナー、催事一覧のサマリーチップ・行バッジ)まで実装済みである点が他の催事系ルールと異なるが、Slack・メールへの自動送信配線が無い点は共通である。催事単位のチャットルーム自動生成は未実装である。
商社(卸)業務(商社(卸)モジュール仕様)との接続では、通知ルール order-reply(納期回答メールの案件紐付け)が担当DM向け、order-stock(欠品・納期遅延アラート)が #発注 チャンネル向けに、それぞれ定義済みである。発注管理・決済フローとの詳細な状態遷移(発注→入荷→請求等)への通知拡張は今後の検討課題とする。
各モジュールは通知イベントを発行するだけとし、配信先の解決は notifications.ts のルールテーブルで一元管理する設計原則をとる。ルールがモジュール横断で1ファイルに集約されている現状は、この原則に沿う。
配信基盤とリアルタイム化
Section titled “配信基盤とリアルタイム化”現状は Step0 に位置づけられる。チャットはフロントの状態管理(zustand + localStorage)のみで、ページを開いている間の表示更新はできるが、他ユーザー・他端末へのリアルタイム配信・プッシュ通知の仕組みはない。
| 段階 | 配信方式 | できること | 実装状況 |
|---|---|---|---|
| Step0(現状) | zustand + localStorage(フロントのみ) | 案件チャットの閲覧・送信・未読表示。Slack通知はタスク依頼のみ実送信 | 実装済(試作範囲) |
| Step1 | バックエンド永続化+ポーリング | 複数端末・複数ユーザーでの状態同期 | 未着手 |
| Step2 | Pusher等リアルタイム配信 | 新着メッセージ・通知のプッシュ配信 | 未着手(採用可否を再評価) |
| Step3 | 双方向同期・AI一次応答 | Slack↔CRM相互反映、質問窓口のAI回答 | 将来構想 |
Pusher採用は旧システム向け見積時点の前提であり、新CRM試作では未採用である。自前WebSocketやSupabase Realtime等との比較は未実施とする。メッセージと通知ログの保持期間は未定である。
永続化とデータ整合性
Section titled “永続化とデータ整合性”useChats は zustand の persist ミドルウェアで localStorage キー crm:chats(version: 3)に保存する。migrate 関数は指定されていないため、保存済みデータの version が現在の 3 と一致しない場合(例:旧バージョンの試作端末に残ったデータ)は移行されず、コンソールにエラーログを出したうえでルームは初期シード(CHAT_THREADS)にフォールバックする。これは意図的な移行設計ではなく、ライブラリ既定の「migrate未指定時は再ハイドレートを諦める」挙動に依存したものである。バックエンド永続化に移行する際は、この暗黙のフォールバックに依存しない移行方針を別途定める必要がある。
ロール別の可視スコープは visibleScopes() により自動的に絞り込む。権限判定に使うのは既存CRMのロール体系(lib/permissions.ts の Role:管理者/営業/事務(BO)/発注・申請担当/代理店/代理店(メンバー)/施工店の7種)のみであり、チャット専用の権限ロールは追加しない。これとは別に、ChatMember.role(lib/chat.ts の ChatRole:営業/事務/施工店/代理店の4種)は発言者アイコン・ラベルを出し分けるための表示専用の型で、権限判定には関与しない。
| ロール | 閲覧できるルーム(visibleScopes) |
|---|---|
| 管理者・営業・事務(BO)・発注・申請担当(社内ロール) | 社内/施工店/代理店の3スコープすべて |
| 代理店/代理店(メンバー) | 代理店ルームのみ |
| 施工店 | 施工店ルームのみ |
サイドバー・案件詳細タブの可視性は lib/permissions.ts の NAV_ALLOW/visibleDealTabs が根拠であり、ロールごとに次の差がある。
| ロール | /chat(受信箱)ナビ | 案件詳細「チャット」タブ |
|---|---|---|
| 管理者 | 表示(全画面可) | 表示 |
| 営業・事務(BO)・発注・申請担当 | 表示 | 表示 |
| 代理店 | 表示 | 表示 |
| 代理店(メンバー) | 非表示(NAV_ALLOW は /events のみ) | 表示(visibleDealTabs は overview/process/chat) |
| 施工店 | 表示 | 表示(overview/process/chat) |
代理店(メンバー)はサイドバーから /chat 受信箱には到達できないが、案件詳細のチャットタブ経由では代理店ルームを閲覧・送信できる。これはナビ設計上の意図的な絞り込み(受信箱型の横断ビューは管理者的な操作に寄るため現場スタッフには出さない)であり、データ側の可視制御(visibleScopes)は代理店ロールと共通である。
いずれの制御もアプリケーション層(React コンポーネントとルート定義)のみで完結しており、ルート(/chat)自体にサーバーサイドのアクセス制御・認可チェックは実装されていない。表示ロールはサイドバーの「表示ロール(デモ)」で誰でも切り替えられるクライアント側の値であり、permissions.ts のコメントにあるとおり、実装では Supabase の RLS + アプリ層のガードで担保する想定である。
外部(商社・代理店)ユーザーのログイン主体・アカウント発行方式は、役割・権限で定める所属・組織軸(Google Workspace正)の設計と合わせて別途定める。
エッジケース・異常系
Section titled “エッジケース・異常系”- ルーム0件: 案件にまだルームが無い場合、
DealChatTabは案内文のみを表示し、内部ロールのみ「社内ルームを開始」ボタンを出す。/chat受信箱側は当該案件を一覧に含めない(ルームが1件も無い案件は集計対象外)。 - 送信者ロールの固定表示:
sendMessage()は送信者のChatMessage.roleを常に"営業"に固定する。表示ロールを施工店・代理店に切り替えて送信しても、メッセージの発言者バッジは「営業」のまま表示される。運用上、施工店・代理店本人が送信したメッセージと社内担当者のなりすまし表示を見分けられない。 - 未読カウンタが増えない: メッセージ送信は
unreadを加算しない。シードの初期値以外で未読が増える経路が無いため、実運用イメージ(相手が送るとこちらの未読が増える)とは挙動が異なる。単一クライアントのモックであることに起因する。 - Slack送信の成否誤検知:
TaskRequestSheetはmode: "no-cors"でPOSTするため、Slack側がWebhook無効・チャンネル権限なし等のエラーを返してもクライアントからは検知できず、成功トーストが出てタスクも作成される。実際にSlackへ届いたかどうかはSlack側で確認するほかない。 - Slack送信失敗時はタスクも未作成:
fetchが例外を投げた場合(不正なURL・ネットワーク不通など)はcatchに入り、後続のaddTask()が実行されない。依頼自体が成立しないため、担当割当も行われない。 - 担当候補が0件:
staffCandidates()が空配列を返す場合(該当ロールのBO_STAFFが存在しない)、selectedはundefinedになり「依頼する」ボタンは非活性のまま操作できない。 - 通知マトリクスとタスク依頼の不整合: 設定画面でどの通知ルールをOFFにしても、
TaskRequestSheetの送信可否には影響しない(前述)。運用者が「OFFにしたのに通知が来る/ONなのに来ない(別経路のため)」と誤解する可能性がある。 - ルート直叩きでのアクセス:
/chatにサーバー側の認可がないため、ナビから非表示のロール(例:代理店(メンバー)が仮に直接URLへ遷移した場合)でもvisibleDeals/visibleScopesによるデータフィルタ自体は効くため、閲覧できる範囲は自ロールの可視範囲に収まる。ただし「ナビに出さない」という意図に反してページ自体には到達できてしまう。 - バージョン不整合時のデータ消失に見える挙動: 前述のとおり
crm:chatsのversionが一致しないと、ユーザーが送信した会話(ローカルのみに保存)を含めて初期シードに戻る。移行なしにバージョンを上げる変更は、試作利用者のローカルデータを黙って失わせる。 - 長文・特殊文字:
ChatMessage.textに長さ上限・サニタイズ処理は無い。表示側はwhitespace-pre-wrapのため改行はそのまま表示されるが、極端な長文でも折り返し表示以外の制御(省略・展開)は無い。
- チャットメッセージ送信時のSlack通知(
chat-messageルール)を配線するかどうか - バックエンド永続化(Supabase等)への移行時期、および移行時のバージョン管理・データ移行方針
- 第三者を既存ルームへ追加する招待フローのUI設計
- ユーザー別既読モデル(
ReadReceipt)への拡張要否 - 未読カウンタをメッセージ送信時に自動加算する仕組みへ拡張するか(現状はルーム単位の単純カウンタで、送信時の自動加算がない)
- メッセージの送信者ロール(
ChatMessage.role)を実ロールに追従させるか(現状は「営業」固定) - 質問窓口スレッドの新規実装可否とスケジュール
- Pusher採用可否の再評価、および自前WebSocket・Supabase Realtime等との比較
- メッセージ・通知ログの保持期間
- 通知マトリクスのON/OFFを実際の送信可否に接続する対象範囲(現状はタスク依頼フローと無関係)