コンテンツにスキップ

顧客・商談CRM

位置づけ: Phase 1(コア)=2026-09-09 決定の開発範囲の本丸(リリース計画 §2026-09-02)。本ページの後半(契約後チェック・施工・発注・申請・入金予定)は後続フェーズ。

顧客・商談CRMは、顧客情報を世帯(Household)単位で管理し、訪問販売・催事・反響・パートナーの全チャネルを1本の商談パイプラインで扱う。前確(訪問前確認)架電、契約後チェック(ローン審査・団信)、施工進捗、発注、申請、経理向け入金予定までを商談(Deal)配下の独立した軸として持ち、業務全体を1画面から追跡できるようにする。

先行リリース(催事+アイテム+商社)の後続フェーズに位置づけ、催事モジュールで獲得したアポの正式な取り込み先、商社(卸)モジュールの発注・仕入先情報の受け皿となる。試作実装(crm_app/app/src/lib/crm.ts、store/useDeals.ts、store/useHouseholds.ts ほか)で型定義・状態遷移・主要画面が動作しており、以下は試作の挙動に基づく。認証・DBは未接続(CURRENT_USER は固定値、永続化は localStorage)で、実データ配線(Supabase / Google 連携)は今後の対応。

  • 対象チャネル(Channel)は訪問販売・催事・反響・パートナーの4種。テレマは現状実施しておらず対象外。
  • 対象範囲は、顧客・世帯・人物、商談パイプライン、活動ログ・案件ロール・マイルストーン、前確・契約後の架電、契約後チェック、施工、発注、申請、経理向け入金予定、担当者アサインまで。
  • 価格・見積・粗利の算出ロジックは 見積・価格表 が正本。本ページでは商談が保持する金額項目(提案金額・仕切り・粗利)の位置づけのみ扱う。
  • KPI集計・予実管理は KPI・予実ダッシュボード が正本。本ページは集計元となる案件ロール・失注理由の保持方法のみ扱う。

先行モジュールとの接続点は次のとおり。

接続点先行側後続側(本ページ)内容
催事アポの取り込み催事モジュール(/events)Deal(status=アポ, channel=催事)催事で獲得したアポを商談として正式登録する。催事シフト詳細の「この催事のアポを商談登録」ボタンから、NewDealSheet を channel=催事・venue=当該店舗名 のプリフィルで起動する導線を実装済み
商社・発注商社(卸)モジュール(/wholesale)DealItem.supplier / Order / LeadtimeInfo商材の仕入先・発注状況を案件配下で管理する
代理店(卸)案件の相互参照商社(卸)モジュール(/wholesale)Deal.agency ↔ WholesaleCase.dealer案件詳細の「関連する卸案件」カードで、代理店名を共通キーに卸案件を参照表示する(別軸維持・FK連結なし)
価格・商流別価格アイテムマスタ(価格非保持)Deal.proposal / Deal.cost / Deal.margin商談の提案金額・仕切り・粗利として反映する
KPI集計—Roles / lostReason案件ロール・失注理由をKPI予実の分析軸として供給する

世帯配下に人物(Person)を複数ぶら下げる。商談応対者(Deal.respondentId)と契約名義人(Deal.contractorId)は人物FKとして分離し、応対した人と契約する人が異なるケースを扱う。物件(Property)は世帯に紐づくマスタではなく商談ごとに埋め込みで独立保持する(Deal.property)。同一世帯で複数商談が走ると築年・面積等を商談ごとに入力し直すことになり、世帯内での不整合(例: 商談Aは築年2003年・商談Bは未入力)が起こり得る。

Household(世帯)

フィールド型・enum必須既定制約・備考
idstring必須—主キー(例 H001)
namestring必須—世帯名(代表姓)。一覧・検索の見出しに使用
postal / pref / city / addrstring必須—郵便番号・都道府県・市区町村・番地。形式バリデーションなし
adults / childrennumber必須—大人・子ども人数。補助金・ローン審査の事前判定材料
taxableboolean必須—課税有無
memostring任意なし運営メモ(在宅時間帯・連絡のつきやすさ等の自由記述)
personsPerson[]必須[]世帯配下の人物一覧

Person(人物)

フィールド型・enum必須既定制約・備考
idstring必須—主キー(例 P001)
name / kanastring必須—氏名・フリガナ
relation"夫" | "妻" | "子" | "本人" | "その他"必須—続柄。世帯代表者の導出に使用
birthstring任意なし生年月日(ISO日付想定)
mobile / tel / emailstring任意なし連絡先。個人単位で複数手段を保持。少なくとも1件必須という型上の制約はない

世帯の表示名(householdName())は「本人 → 夫 → 世帯の先頭(persons[0])」の優先順で代表者を選び、フルネームをそのまま表示する(敬称は付けない)。妻・子・その他のみで構成される世帯(本人・夫が不在)では persons[0] にフォールバックするため、並び順次第で子どもの氏名が世帯代表として表示され得る。

Property(物件)/ Existing(既設設備)

フィールド型・enum必須既定制約・備考
builtYearnumber任意なし築年
areanumber任意なし面積(㎡)
layout / pinTypestring任意なし間取り・種別(自由入力)
existingExisting[]必須[]既設設備一覧
Existing フィールド型・enum必須既定制約・備考
kind"太陽光" | "エコキュート" | "IH" | "ガス給湯器"必須—既設設備の種別
hasboolean必須—設置有無
datestring任意なし設置年("YYYY-MM" 表記が実データ上の慣例。型上は自由文字列)
makerstring任意なしメーカー
capacitystring任意なし容量(自由記述。例 "4.2kW")
countnumber任意なしパネル枚数等

既設太陽光がある案件(変更認定)では、委任状・印鑑証明は「既設太陽光の名義人」のものが必要になり、Deal.contractorId(今回契約の名義人)と一致しない場合があることを APPLICATION_DOC_MASTER の備考で明示している(後述)。

フィールド型・enum必須既定制約・備考
idstring必須—主キー(例 D-2406-001)
statusDealStatus(9値。後述)必須"アポ"(新規登録時)パイプライン+分岐・終了状態
channel"訪問販売" | "催事" | "反響" | "パートナー"必須—商流(獲得の入口)
hankyoMediastring任意なしchannel="反響" のときの大元媒体。HANKYO_MEDIA のプルダウン候補+自由入力を許容
subject"自社" | "代理店"任意未設定時は "自社" 扱い(subjectOf())実施主体。登録時にログインユーザーの所属から自動判定(手選択しない)
agencystring任意なしsubject="代理店" のときの代理店名
venuestring任意なしchannel="催事" のときの催事場所。マスタFKではなく自由文字列
householdIdstring必須—世帯FK
propertyProperty必須—商談ごとに独立保持(世帯マスタとは別実体)
rolesRoles必須{}案件ロール(獲得/前確/クローザー/設計)。ユーザー名の自由文字列
respondentId / contractorIdstring任意なし商談応対者/契約名義人の人物FK
itemsDealItem[]必須[]商材明細
milestonesMilestone[]必須defaultMilestones()(6項目・全て"未")工程マイルストーン
tasksDealTask[]必須新規登録時 [{type:"前確", assignee:"", status:"未着手"}]案件タスク
notesDealNote[]任意なし(addNote 時に [] から生成)活動ログ
postContractPostContract任意成約時に defaultPostContract(true) で自動生成契約後チェック
constructionConstruction任意工事時に defaultConstruction() で自動生成施工フラグ
paymentPayment任意なし決済・入金
ordersOrder[]任意なし(addOrder 時に [] から生成)発注
applicationsApplication[]任意なし(addApplication 時に [] から生成)申請
proposal / cost / marginnumber任意なし提案金額(税込想定)・仕切り(原価)・粗利。自動計算関数は crm.ts に存在しない(後述「金額項目」)
memostring任意なし運営メモ(自由記述)
lostReasonstring任意(型上)なし失注・前確NGの理由。型は任意だが、失注操作のUI(ConfirmActionDialog)では入力必須(reasonRequired)に実装済み
createdAtstring必須登録日作成日(ISO日付)
closeDatestring任意成約時に未設定なら当日を自動設定成約日

パイプラインは全チャネル共通の1本。channel は入口属性としてのみ保持し、ステータスをチャネル別に分岐させない。

ステータス意味主担当ロール
アポアポ獲得(入口フォーム送信直後)獲得
前確訪問前確認の架電中前確担当(バックオフィス)
商談前確OK後の訪問・クロージングクローザー
成約契約成立。契約後チェックを自動生成クローザー/設計
工事施工フェーズ。施工フラグを自動生成設計/施工店
完了全工程終了バックオフィス
保留商談後保留。リマインド対象として追い続けるクローザー
失注前確NG・商談NG等。lostReason を保持する運用前確担当/クローザー
クーリングオフ成約後の申込撤回(終了状態)バックオフィス

主パイプラインは「アポ→前確→商談→成約→工事→完了」の6状態(DEAL_STAGES)で、advanceStage はこの順で1段階ずつ進める(現在ステータスが DEAL_STAGES に含まれない、または既に「完了」のときは何もしない)。保留・失注・クーリングオフはこの並びの外側にある分岐・終了状態で、setStatus による明示的な変更で遷移する。ストア層の setStatus 自体は遷移元・遷移先の組み合わせを制限しない(任意ステータス間を直接遷移できる)。遷移の妥当性は UI 側の導線設計(後述「状態遷移・ワークフロー」「画面・操作の挙動」)で担保している。

DealItem(商材明細)

フィールド型・enum必須既定制約・備考
kind"PV" | "BT" | "EQ" | "IH" | "AC" | "付帯"必須—商材種別
maker / model / spec / warranty / supplierstring任意なしメーカー・型番・仕様・保証・仕入先
deliveryDatestring任意なし納品予定日

Milestone(工程マイルストーン)

フィールド型・enum必須既定制約・備考
keystring必須—defaultMilestones() が生成する標準キー: genchou(現調)/ chakkou(着工)/ kankou(完工)/ renkei(連携)/ nintei(設備認定)/ nyukin(入金)
labelstring必須キーに対応する日本語ラベル画面表示名
status"未" | "進行" | "完了"必須"未"
datestring任意なし到達日

DealTask(案件タスク)

フィールド型・enum必須既定制約・備考
type"前確" | "太陽光調査設計"必須新規登録時は "前確" を自動生成タスク種別
assigneestring必須""(未割当)バックオフィス担当。ユーザーFKではなく自由文字列
status"未着手" | "予定" | "進行" | "完了" | "NG" | "不出"必須"未着手"
duestring任意なし期日("YYYY-MM-DD HH:mm" 表記の慣例)
resultstring任意なし架電・作業結果。自由記述文字列であり CallResult 型そのものではない。「OK」等を入れる運用だが型としては任意の文字列を許容する
driveUrlstring任意なし成果物のDriveリンク
gate"初回前" | "クロージング前"任意新規登録時の自動生成タスクでは未設定前確タスクのみ使用(前確以外では未使用)。前確は「初回訪問の前」「クロージング訪問の前」の2箇所にあり、パイプライン上のステータスは分岐させず本フィールドで区別する。新規案件で自動生成される最初の前確タスクは gate 未設定のまま生成される(初回前を意味する運用だが、値としては明示されない)

Roles(案件ロール)

フィールド型必須既定制約・備考
獲得 / 前確 / クローザー / 設計string任意なしいずれもユーザー名の自由文字列。分業体制(獲得担当と成約担当が別人)を前提に4種を分離。ユーザーFK化は将来対応の予定

DealNote(活動ログ)

フィールド型必須既定制約・備考
idstring必須`N${Date.now()}`addNote() が自動採番
datestring必須当日ISO日付addNote() が自動設定
authorstring必須CURRENT_USER.nameログインユーザー固定(実認証接続は今後)
textstring必須—自由記述

契約後チェック・施工・決済・発注・申請・入金予定

Section titled “契約後チェック・施工・決済・発注・申請・入金予定”

PostContract(契約後チェック)— 成約時に defaultPostContract(loanNeeded) で自動生成

フィールド型・enum必須既定制約・備考
thanksCallCallResult("未" | "不出" | "OK" | "NG")必須"未"サンキューコール(社内・クレーム予防)
thanksCallDate / thanksCallNotestring任意なし
loanLoanStatus("対象外" | "書類準備" | "審査中" | "不備あり" | "審査NG(再審査予定)" | "審査OK" | "審査NG")必須ローン要否により "書類準備" または "対象外"ローン審査(書類PDF→FAX送信→結果)
loanCompany / loanNotestring任意なしローン会社名(マスタ選択想定だが型は自由文字列)・不備内容など
reviewCallCallResult必須"未"審査確認コール(ローン会社実施。正式可否とは別フラグ)
danshinDanshinStatus("対象外" | "未着手" | "手続中" | "完了")必須ローン要否により "未着手" または "対象外"団信

CallResult の「不出」は実運用スプレッドシートの語彙をそのまま採用している。

Construction(施工)— 工事時に defaultConstruction() で自動生成

フィールド型・enum必須既定制約・備考
vendorstring任意なし施工店
vendorTool"CRMチャット" | "ANDPAD" | "LINE"任意なし施工店との連絡手段
requestSheetAtstring任意なし現地依頼書の送付日
surveyDoneboolean必須false現地調査の実施フラグ
surveyDatestring任意なし
scheduleStatus"調整前" | "現調・工事日時調整中" | "現調日確定・工事日時調整中" | "現調・工事日時調整済み"任意なし日程調整(実物の語彙をそのまま採用)
photosStoredboolean必須false現地写真の格納(漏れがあれば通知対象)
completionReport"未" | "催促中" | "受領"必須"未"施工店からの完了報告。完工書とは別物(現地情報がないと請求できない)
completionReportAt / completionNotestring任意なし
kankoCallCallResult任意なし完工コール(完工後の顧客確認+完工書返送の督促)
kankoCallDatestring任意なし
kankosho"未" | "立会時回収" | "レタパ送付済み" | "原本回収済み"任意defaultConstruction() では未設定のまま("未" を明示的に入れない)完工書(顧客サインの工事完了証明)。ローン会社へ提出しないと入金されない最重要書類
kankoshoAtstring任意なし原本回収(信販会社到着)日。入金予定算出の起点
renkeiTachiai"未定" | "立会あり" | "立会なし"任意"未定"立会有無(電力会社の現地確認)。有無で連絡経路が変わる
renkeiStatus"対象外" | "調整前" | "調整中" | "連携済"任意"調整前"連携日調整の状態
renkeiDatestring任意なし連携日=売電開始日

立会あり=電力会社が施工店へ連絡→施工店→自社、立会なし=勝手に連携され結果が後から来る運用で、結果(連携日)が施工店の連絡漏れで来ないことが多い。完工書は、立会あり→立会時回収/立会なし→返送用レターパック同封で送付→完工コールで督促→原本回収、という経路をたどる。

Payment(決済・入金)

フィールド型・enum必須既定制約・備考
method"現金一括" | "現金半金" | "ローン"必須—
settlement"2社間" | "3社間"任意なしローン時のみ意味を持つ。3社間=商社の売掛スキーム(金額同じ・入金が遅い)
downPaymentnumber任意なし頭金(ローン時・顧客から直接、工事前)
loanTermMonthsnumber任意なしローンの支払回数(月数)
upfront / balancenumber任意なし前金(現金半金・着工1週間前)/後金(現金半金・完工後)。入金タイミングの定義は未確定のため、経理向け入金予定(receivables())の対象外

Order(発注)

フィールド型・enum必須既定制約・備考
idstring必須—主キー
kindItemKind必須—
maker / model / supplierstring任意なし
status"未発注" | "発注済" | "納期回答済" | "納品済"必須—
orderedAt / dueDate / deliveredAtstring任意なしdueDate は納期回答後に確定
mailLinkedboolean任意なし納期回答メールを案件へ自動紐付け済みか
notestring任意なし

Application(申請)/ AppDoc(申請書類)

Application フィールド型・enum必須既定制約・備考
idstring必須—主キー
typeApplicationType(9値。後述)必須—
status"未着手" | "書類収集中" | "申請中" | "差戻し" | "完了"必須newApplication() で "未着手"
docsAppDoc[]必須APPLICATION_DOC_MASTER[type] から自動展開(collected: false)
deadlinestring任意なし申請期限(例: 延長保証=連携月の翌月5日)
linkedAtstring任意なし連携日(経産省・電力会社からの返答日)
notestring任意なし

ApplicationType の9種: 新規認定 / 変更認定(事前) / 変更認定(事後) / 変更認定(通常) / 補助金 / メーカー保証 / 発電所保証(JPEA) / 設置トラブル対応窓口 / 延長保証。

AppDoc フィールド型必須既定制約・備考
namestring必須—書類名
collectedboolean必須falsetoggleAppDoc() でトグル。取得済みに切り替えると同時に obtainedAt を当日で自動記録
obtainedAtstring任意なし取得日。有効期限のある書類でのみ使用
validDaysnumber任意なし有効日数。公的書類は発行から90日(3ヶ月)想定
optionalboolean任意未設定=◎必須true=△(該当の場合のみ必要)
notestring任意なし名義人の注意・取得先など

必要書類は APPLICATION_DOC_MASTER により申請種別ごとに定義済みで、変更認定(事前/事後/通常)は共通の HENKO_DOCS(委任状2種・印鑑証明・設備ID・受給開始日資料)を参照する。設置トラブル対応窓口の書類は原発言未確認のため仮置き(コード上に明記)。10kW超・法人申請は書類が異なるが未反映。

Receivable(入金予定・算出専用)— receivables() の戻り値。永続化はされず、都度算出される

フィールド型算出式・備考
dealDeal参照元の案件
duestringpaymentDueDate(d, loanCompanies) の結果
overduebooleantoday > due
overdueDaysnumber超過時のみ算出(Math.floor((today - due) / 86_400_000))
amountnumberMath.max(0, (proposal ?? 0) - (downPayment ?? 0))
loanCompany / settlementstring / SettlementpostContract.loanCompany / payment.settlement をそのまま転記
stateDiagram-v2
    [*] --> アポ: 新規登録(前確タスク自動生成)
    アポ --> 前確: 前確タスクへ担当割当(自動連動)
    前確 --> 商談: 前確 OK(自動連動)
    前確 --> 失注: 前確 NG(確認・理由必須)
    アポ --> 商談: 前確OK(担当割当なく直接OKにした場合)
    アポ --> 失注: 前確 NG(確認・理由必須)
    商談 --> 保留: クイック変更
    商談 --> 失注: 失注(確認・理由必須)
    保留 --> 商談: クイック変更(再開)
    保留 --> 失注: 失注(確認・理由必須)
    商談 --> 成約: 案件詳細(確認あり)
    成約 --> 工事: advanceStage / setStatus
    成約 --> クーリングオフ: 案件詳細(確認・理由必須)
    工事 --> クーリングオフ: 案件詳細(確認・理由必須)
    完了 --> クーリングオフ: 案件詳細(確認・理由必須)
    工事 --> 完了: advanceStage
    失注 --> [*]
    クーリングオフ --> [*]
    完了 --> [*]

主パイプライン(アポ→前確→商談→成約→工事→完了)は advanceStage で前進のみを行う(逆戻り不可)。分岐・終了状態(保留・失注・クーリングオフ)は setStatus の直接呼び出しで遷移する。ストア層の setStatus 自体は遷移元・遷移先を制限しないため、上図はストアの制約ではなくUI側の導線として提供されている遷移を表す(一覧画面のクイック変更ポップオーバーは アポ/前確/商談/工事/保留 の直接切替と 失注(確認)のみを提供し、成約/完了 とクーリングオフは案件詳細画面経由に限定する。案件詳細では、成約は支払方法選択つきの確認、完了は未達項目(doneBlockers)がある場合のみ警告ダイアログを挟み、なければ即時変更+取消トーストで反映する。クーリングオフは成約・工事・完了の案件でのみ選択できる(allowCoolingOff)。詳細は「画面・操作の挙動」)。

前確ゲート(gate: 初回前/クロージング前)はステータスとは独立した軸で、前確は「初回訪問の前」と「クロージング訪問の前」の2箇所にあるが、パイプライン上のステータスはどちらも「前確」のまま分岐させない。

前確結果の確定によるステータス自動連動(案件詳細タスクカード)

Section titled “前確結果の確定によるステータス自動連動(案件詳細タスクカード)”
sequenceDiagram
    participant U as 担当者(案件詳細)
    participant UI as タスクカード(record-ui.tsx)
    participant Deals as useDeals

    U->>UI: 前確タスクの結果を入力
    alt 「OK」で確定
        UI->>Deals: updateTask(dealId, taskIdx, {status:"完了", result:"アポ確定(+日付)"})
        UI->>UI: linkStatus = deal.status が「アポ」または「前確」か判定
        alt linkStatus = true
            UI->>Deals: setStatus(dealId, "商談")
            UI-->>U: トースト「前確 OK → 商談へ」+取消ボタン
        else linkStatus = false(成約済み等の古いタスク)
            UI-->>U: トースト(ステータス変更なし)
        end
    else 「NG」(確認ダイアログ・理由必須)
        U->>UI: NG理由を入力して確定
        UI->>Deals: updateTask(dealId, taskIdx, {status:"NG", result:"前確NG"})
        alt linkStatus = true
            UI->>Deals: setStatus(dealId, "失注") + updateDeal({lostReason})
        end
        UI-->>U: トースト「前確 NG → 終了」+取消ボタン
    end

架電ボード(CallsPage.tsx)は上記の linkStatus 判定を持たない。OK確定は常に updateTask({status:"完了", result:"OK"})+setStatus(dealId, "商談")、NG確定(確認ダイアログ・理由必須)は常に setStatus(dealId, "失注")+updateDeal({lostReason}) を実行する。同一ステータスへの setStatus はストア側で無視されるため商談中案件のOKでは実害がないが、商談以降の案件に未完の前確タスクが残っている場合、架電ボードからのNGはステータスを失注へ動かし得る(タスクカード側とは挙動が一致していない)。

架電ボードでタスクへ担当者を割り当てた時点では、案件が「アポ」のときに限り setStatus(dealId, "前確") が実行され、案件は自動的に「前確」へ進む(それ以外のステータスでは担当割当のみでステータスは変わらない)。

ステータス遷移の副作用(成約・工事)

Section titled “ステータス遷移の副作用(成約・工事)”
sequenceDiagram
    participant U as ユーザー
    participant UI as 案件詳細 / 一覧
    participant Deals as useDeals
    participant Chats as useChats

    U->>UI: ステータスを「成約」に変更
    UI->>Deals: setStatus(dealId, "成約")
    Deals->>Deals: withStatus(): closeDate 未設定なら当日を設定/postContract 未設定なら defaultPostContract(true) で生成
    Deals->>Chats: createRoomsFor(dealId, {agency, scopes:["社内", (代理店案件なら)"代理店"]})

    U->>UI: ステータスを「工事」に変更
    UI->>Deals: setStatus(dealId, "工事")
    Deals->>Deals: withStatus(): construction 未設定なら defaultConstruction() で生成
    Deals->>Chats: createRoomsFor(dealId, {vendor, scopes:["施工店"]})

createRoomsFor は同一 dealId×scope の部屋が既に存在すれば作成をスキップする(冪等)。案件詳細からの成約は CloseWonDialog で支払方法(ローン/現金)を確認してから setStatus を呼び、現金の場合は直後に updatePostContract({loan:"対象外", danshin:"対象外"}) と payment.method の設定(現金一括)を行う(ストアの初期値自体はローン前提の defaultPostContract(true))。

sequenceDiagram
    participant Acc as 経理ユーザー
    participant UI as 入金予定ビュー
    participant Fn as receivables() / paymentDueDate()
    participant Masters as LOAN_COMPANIES

    Acc->>UI: 入金予定画面を開く
    UI->>Fn: receivables(deals, loanCompanies, today)
    loop 案件ごと
        Fn->>Fn: payment.method==="ローン" かつ construction.kankoshoAt あり かつ 入金マイルストーン未完了 のときのみ due を算出
        Fn->>Masters: settlementDays(d): ローン会社マスタ一致で daysDirect(2社間)/ daysViaTrader(3社間)、未一致は 2社間3日/3社間6日にフォールバック
        Fn->>Fn: addBusinessDays(kankoshoAt, 営業日数):土日・2026年祝日マスタを除外して加算
    end
    Fn-->>UI: 予定日昇順の Receivable[](today > due の案件は overdue=true)
    UI-->>Acc: 超過案件を強調表示
画面内容
入口フォーム(新規案件、NewDealSheet)商流プリセットで商流固有項目を出し分けて登録。initial(channel/venue/source)でのプリフィルと trigger(起動ボタンの差し替え)に対応し、催事シフト詳細など他画面からトリガー起動できる汎用コンポーネントとして実装済み
世帯・人物管理世帯配下の人物一覧、続柄・連絡先の編集、物件の既設設備登録
商談パイプラインボードステータス別の案件一覧、advanceStage による段階遷移、保留・失注・クーリングオフへの分岐操作
案件詳細活動ログ・案件ロール・マイルストーン・商材明細を1画面に集約。右レールに「関連する卸案件」カード(d.agency が卸案件の一致で表示。0件/未設定は非表示)を実装済み
架電(前確)ボード未完(未着手・予定・進行)の前確タスクを期日順に「期限超過・本日・以降」へ分けて一覧化し、担当割り当てと架電結果(DealTask.result。自由記述文字列)を記録
契約後チェックサンキューコール・ローン審査・団信・審査確認コールの進捗を独立管理
施工管理日程・依頼書送付・現地調査・写真・完了報告・完工書回収・連携日調整をフラグで管理
発注管理商材別の発注状況、商品マスタ優先・メーカー目安フォールバックの納期表示、着工日からの発注期限逆算
申請管理申請種別ごとの必要書類照合、公的書類の失効期限による優先度表示
入金予定(経理向け横断ビュー)ローン案件のうち完工書原本回収済み・入金未完了のものを予定日順に一覧表示し、超過分を強調
担当者アサイン職能ロール(前確/設計)に合う候補者を稼働中タスク数の少ない順に提示し、Slack通知プレビューを生成
  • 一覧のクイック変更(DealsPage.tsx): 行ホバーで表示される「変更」ポップオーバーは、アポ/前確/商談/工事/保留(QUICK_STATUSES)への即時切替+取消トーストと、失注(ConfirmActionDialog・理由必須)を提供する。成約/完了(LOCKED_STATUSES)はグレーアウトで選択不可とし、「成約・完了・クーリングオフは案件詳細から(確認あり)」という注記を表示する。
  • 案件詳細のステータス終了操作(DealDetail.tsx): 失注・クーリングオフへの変更は ConfirmActionDialog(reasonRequired)を経由し、理由未入力では確定できない。
  • 案件詳細のステータス変更(DealDetail.tsx): 成約は CloseWonDialog(「成約にしますか?」・支払方法の選択つき・理由入力なし)、完了は未達項目(doneBlockers)がある場合のみ警告ダイアログ(「未完了の項目があります」)を挟む。それ以外のステータスは即時変更+取消トースト。
  • 架電ボード・タスクカードの前確NG(CallsPage.tsx / record-ui.tsx): NG確定は理由必須の確認ダイアログを経由する。タスクカード側は案件が「アポ」または「前確」のときのみ失注へ連動し、架電ボード側は判定なしで常に失注へ連動する(前掲シーケンス参照)。
  • 発注管理: 型番一致の商品マスタ(Product.leadTimeDays)を最優先とし、resolveLeadtime() が一致すれば source:"商品マスタ"、なければ種別×メーカーのメーカー目安(LEADTIME_MASTER、source:"メーカー目安")にフォールバックして表示する。いずれも一致しなければ納期は非表示。発注確定操作(「発注しますか?」)は確認ダイアログを経由する。
  • 施工管理: 現地依頼書の送付(「現地依頼書を送付しますか?」)、完工書レターパックの送付は、いずれも確認ダイアログを経由する(ConstructionPanel.tsx)。納品確認・現調完了等の軽微な更新はチェック操作のみで確認なし。
  • 入金予定(経理向け横断ビュー): receivables() の結果を予定日昇順に表示し、overdue=true の行を強調する。
  • 担当者アサイン: staffCandidates(type, liveDeals) は職能ロール(前確/設計)に合う BO_STAFF を、未完タスク数(status が 完了/NG 以外)の少ない順に並べて提示する。割当自体は自動化せず、一覧を見た担当者・管理者が手動で選ぶ(後述「業務ルール」)。選定後 slackMessage() が通知文プレビューを生成する。
  • ステータスを「成約」に変更すると、closeDate(成約日)が未設定なら当日を自動設定し、postContract が未設定ならローン前提の初期値(defaultPostContract(true))で自動生成する。
  • ステータスを「工事」に変更すると、construction(施工フラグ)が未設定なら初期値(defaultConstruction())で自動生成する。
  • 成約時は社内チャットルームを自動作成し、代理店案件(subject=代理店)の場合は代理店ルームも合わせて作成する。工事時は施工店チャットルームを自動作成する。既存の同一スコープのルームがあれば再作成しない。

前確(訪問前確認)と契約後コール

Section titled “前確(訪問前確認)と契約後コール”
  • 前確は初回訪問の前とクロージング訪問の前の2箇所にあり、パイプライン上のステータスは分岐させず、前確タスクのゲート(初回前/クロージング前)で区別する。
  • サンキューコール・審査確認コールの結果はCallResult型で未/不出/OK/NGの4値に固定される。「不出」は実運用スプレッドシートの語彙をそのまま採用している。前確タスク(DealTask)の結果はresult(自由記述文字列)に記録し、CallResult型そのものではない。「OK」のような値を入れる運用だが、型としては任意の文字列を許容する。
  • 契約後もコールは続き、サンキューコール(クレーム予防、社内実施)と審査確認コール(ローン会社実施、正式可否とは別フラグ)を独立して記録する。
  • 新規案件は登録時に前確タスクが自動生成され(gate 未設定)、担当割当時にステータスがアポ→前確へ、結果確定時に前確→商談/前確→失注へ自動連動する。案件詳細タスクカードでは連動条件が現在ステータス「アポ」または「前確」のときに限定され、成約済み案件に残る古い前確タスクの結果入力ではステータスを巻き戻さない(linkStatus 判定)。架電ボードにはこの判定がなく、結果確定は常にステータスへ連動する(同一ステータスへの変更はストア側で無視)。

誤操作ガード・確認ダイアログ

Section titled “誤操作ガード・確認ダイアログ”

基本操作(クイック変更のステータス切替、チェック系フラグの更新等)は即時反映+取消トースト(「元に戻す」)で対応する。外部作用を伴う操作は ConfirmActionDialog による確認(理由入力必須のものを含む)を挟む。実装済みの確認ダイアログは次のとおり。

操作画面理由入力
失注への変更一覧クイック変更 / 案件詳細 / 架電ボード・タスクカード(前確NG連動)必須
クーリングオフへの変更(成約・工事・完了の案件のみ選択可)案件詳細必須
成約への変更(支払方法ローン/現金の選択つき)案件詳細なし
完了への変更(未達項目がある場合のみ警告)案件詳細なし
発注の確定発注管理説明表示のみ
現地依頼書の送付施工管理説明表示のみ
完工書レターパックの送付施工管理説明表示のみ
  • 発注は工事日(着工)確定後に行う。
  • 納期表示は、型番一致の商品マスタを最優先とし、一致がなければ種別×メーカーのメーカー目安(LEADTIME_MASTER)にフォールバックする(resolveLeadtime)。メーカー目安側の一致判定は maker 文字列の部分一致(includes)で行う。
  • 発注期限(orderDeadline)は着工日からリードタイム日数を暦日で逆算する(d.setDate(d.getDate() - leadDays))。入金予定の算出(後述)が営業日ベースであるのに対し、発注期限は暦日ベースである点が異なる。
  • 納品確認のチェック欄は置かない。連絡がなければ納品済みとみなし、納期回答メールの自動紐付けで代替する。
  • 必要書類は申請種別ごとのマスター(APPLICATION_DOC_MASTER)と照合し、案件生成時に自動展開する(newApplication())。
  • 公的書類は発行から90日で失効するものとして扱う(AppDoc.validDays)。書類の残日数(docDaysLeft)は obtainedAt と validDays の両方が入力されている書類のみ算出される(collected フラグ自体は参照しない。未取得の書類は通常 obtainedAt が無いため対象外だが、一度取得済みにして未取得へ戻しても obtainedAt は残るため算出対象のままになる)。
  • 申請の緊急度(applicationUrgency)は、status !== "完了" の申請について、各書類の残日数と申請期限(deadline)の残日数のうち最小値(=最も近い期限)を返す。対象がなければ null(緊急度なし=優先度リストに出ない)。
  • 入金予定日(paymentDueDate)は、payment.method === "ローン" かつ完工書の原本回収日(construction.kankoshoAt)があり、nyukin マイルストーンが 完了 以外のときにのみ算出する。完工書の回収状況(kankosho フィールドの値)自体は判定に使わず、kankoshoAt(日付)の有無のみで判定する。
  • 起算は完工書原本回収日(kankoshoAt)からの営業日数(settlementDays)で、ローン会社マスタ(LOAN_COMPANIES)に登録があればその値(2社間=daysDirect/3社間=daysViaTrader)を優先し、なければ2社間3営業日/3社間6営業日を用いる。
  • 営業日加算(addBusinessDays)は土曜・日曜と、ハードコードされた2026年の祝日マスタ(JP_HOLIDAYS_2026)のみを除外する。2026年以外の日付や祝日改定には追随しない(後述「エッジケース」)。
  • 予定日を超過した案件は経理向けビューで優先表示し、信販会社への確認対象として扱う。現金半金の後金(balance)は入金タイミングの定義が未確定のため対象外。

金額項目(提案金額・仕切り・粗利)

Section titled “金額項目(提案金額・仕切り・粗利)”

Deal.proposal(提案金額・税込想定)・cost(仕切り原価)・margin(粗利)は保持するのみで、crm.ts 内に自動計算関数は存在しない。案件編集画面(DealEditSheet.tsx)は proposal - cost を都度計算して表示し、現在の margin 入力値と異なる場合のみ「計算値:◯◯円(適用)」ボタンで手動反映を促す(自動上書きはしない)。価格・見積・粗利の算出ロジックの正本は 見積・価格表 であり、本ページの Deal はその結果を保持する器という位置づけ。

商談⇄卸案件の相互リンク(代理店名でのクロスリンク)

Section titled “商談⇄卸案件の相互リンク(代理店名でのクロスリンク)”

試作実装(crm_app)では、案件詳細と催事シフト詳細(催事運営参照)のそれぞれから、代理店名を共通キーとした卸案件(商社(卸)モジュール)への相互参照リンクを実装済みである。商談(前段)と卸(後段)は別軸のまま維持し、外部キーによる連結や軸の統合は行わない。追加するのは参照リンクのみである。

  • 案件詳細(/deals/$dealId、DealDetail.tsx の DealAgencyWholesaleCard): Deal.agency(代理店名)と、商社(卸)モジュールの卸案件が持つ WholesaleCase.dealer フィールドを完全一致で突合し、一致する卸案件を「関連する卸案件」カードとして件数表示する。d.agency が未設定(subject="自社" の案件等)、または一致する卸案件が0件のときはカード自体を非表示にする。表示時は「卸案件一覧」ボタンから /wholesale へ遷移する。
  • 対称の実装(催事シフト詳細の DealerWholesaleCard、卸案件詳細の DealerEventsCard)は 催事運営 の「催事⇄卸(代理店/販売店名でのクロスリンク)」を参照。三者(催事・商談・卸)は代理店名/店舗名を共通キーとした別軸のクロスリンクで串刺しされている。

突合キーは代理店名の完全一致であり、外部キーでの連結ではない。名称の表記ゆれがあれば突合漏れが生じうる。

入口フォームのプリフィル対応(NewDealSheet)

Section titled “入口フォームのプリフィル対応(NewDealSheet)”

NewDealSheet は initial: {channel, venue, source} と trigger(起動ボタンの差し替え)の2つの props に対応し、他画面から特定の商流・催事場所をプリフィルした状態で新規案件フォームを開ける汎用コンポーネントとして実装済みである。現状の利用箇所は催事シフト詳細の「この催事のアポを商談登録」ボタン(channel="催事"・venue=当該店舗名)のみだが、コンポーネント自体は催事に限定されない汎用のプリフィル機構であり、他画面からの起票導線に転用できる。

CRMには2種類の「ロール」概念があり、混同しないよう区別する。

  • 案件ロール(Roles): 商談に紐づく獲得・前確・クローザー・設計の4種で、担当の表示用ラベル。ユーザー名の自由文字列で保持し(DealTask.assignee も同様に自由文字列)、アクセス制御ではない。ユーザーFK化は将来対応の予定。
  • 画面ロール(lib/permissions.ts の Role): サイドバーの「表示ロール(デモ)」で切り替える、ログインユーザー種別のアクセス制御。7種(管理者/営業/事務(BO)/発注・申請担当/代理店/代理店(メンバー)/施工店)を定義し、ナビゲーション可視性・案件の編集/新規作成可否・金額列や運営メモの表示可否・案件詳細タブ・チャット可視スコープを一括で切り替える。

画面ロールごとの主な可否は次のとおり(根拠: lib/permissions.ts の visibleDeals / canEditDeal / canCreateDeal / canSeeCost / canSeeProposal / canSeeInternalMemo / canExport / visibleDealTabs)。

ロール案件の可視範囲編集新規作成仕切原価・粗利提案金額運営メモエクスポート案件詳細タブ
管理者全件可可表示表示表示可全8タブ
営業全件可可表示表示表示可全8タブ
事務(BO)全件可可表示表示表示可全8タブ
発注・申請担当全件(ただしナビは /deals /orders /applications 等に限定)不可不可表示表示表示可全8タブ
代理店(管理者)自社(subject=代理店 かつ agency 一致)のみ不可可非表示表示非表示不可5タブ(overview/items/process/chat/activity)
代理店(メンバー)自社のみ(ただしナビは /events のみで実質到達不可)不可不可非表示非表示非表示不可3タブ(overview/process/chat)
施工店施工が始まっている担当案件のみ(construction あり)不可不可非表示非表示非表示不可3タブ(overview/process/chat)

代理店(管理者)は自社リードの新規登録は可能だが、登録後の案件編集は不可(canCreateDeal には含むが canEditDeal には含まない非対称な設計)。この画面ロール切替はデモ用のモック実装で、コード冒頭コメントに「実装では Supabase の RLS + アプリ層のガードで担保する」と明記されている。実際の認証(Supabase)・ロール割当・RLSとの接続は未実装。

  • 担当者名の部分一致による誤集計: staffCandidates() の未完タスク数集計は DealTask.assignee.includes(staff.name.split(" ")[0])(姓の文字列部分一致)で行う。assignee はFKではなく自由入力のため、表記ゆれ(フルネーム/姓のみ/別区切り)や同姓の別担当者がいる場合に集計がずれる。
  • 入金予定額のマイナス相殺のみ: Receivable.amount = max(0, proposal - downPayment) で、proposal が未入力(undefined)の案件でも paymentDueDate の条件(ローン・完工書回収済み・入金未完了)を満たせば amount=0 の行として一覧に現れる。
  • 完工書ステータスと回収日の不整合: construction.kankosho(表示用ステータス)と入金予定算出が参照する kankoshoAt(回収日)は別フィールドで、kankosho を更新せず kankoshoAt だけを直接更新すると、画面表示(未回収)と入金予定算出(回収済み扱い)が食い違い得る。
  • 祝日マスタが2026年限定: JP_HOLIDAYS_2026 はハードコードのため、2027年以降の日付で addBusinessDays を使うと祝日を除外できず、土日のみを避けた営業日計算になる(入金予定日が実際より早く算出される)。
  • 世帯代表者の導出フォールバック: householdName() は「本人→夫→persons[0]」の順で決まるため、本人・夫が世帯にいない場合(母子世帯で「妻」区分のみ、または「子」しかいない等)は登録順で先頭に来た人物が世帯代表として表示される。
  • 前確タスクのgate未設定: 新規案件登録時に自動生成される前確タスクは gate が未設定のまま作成される。初回前を意味する運用だが、値としては明示されないため、gate を条件にした集計・表示は未設定分を初回前として扱うか個別に判断が必要。
  • lostReason はUI必須だが型は任意: Deal.lostReason は型上オプショナルで、失注操作の確認ダイアログ(reasonRequired)を経由しない経路(例: updateDeal() の直接呼び出しやシードデータ)では未入力のまま失注状態になり得る。
  • 粗利の未整合: margin は手入力値であり、proposal - cost と自動的に同期しない。編集画面は差分がある場合のみ「適用」ボタンで提示するため、画面外の更新経路(一括インポート等)を通ると proposal - cost と margin が一致しない案件が生じ得る。
  • Order / Application の既定値: Deal.orders / Deal.applications は未設定(undefined)で始まり、addOrder / addApplication の初回呼び出しで [] から生成される。案件作成直後にこれらのフィールドへ直接アクセスするコードは ?? [] 等の防御が必要。
  • advanceStage の無反応ケース: 現在ステータスが 保留/失注/クーリングオフ(DEAL_STAGES に含まれない)、または既に 完了 のときに advanceStage を呼んでも何も起きない(エラーにもならない)。UIボタンの活性制御と一致しているか個別画面での確認が要る。
  • メーカー名の部分一致リスク: leadtimeFor() のメーカー目安(LEADTIME_MASTER)照合は maker.includes(l.maker) で行うため、登録したメーカー名が別メーカー名の部分文字列になっている場合、意図しない一致が起こり得る(現状マスタ件数が少ないため顕在化していない。型番一致の商品マスタ照合は model の完全一致)。

2026-09-10 時点の実装(crm_next feat/customers-core、migration 0018)で確定した、試作からの差分と設計上の決め。

テーブル主キー備考
householdsH0001 形式(id_counters で採番)postal/pref/city/addr は DB 上 nullable(必須は zod で担保=欠損があっても一覧が破綻しない)
personsP0001 形式(世帯をまたいでグローバル採番・id_counters)household_id FK。relation は pgEnum 5値
dealsD-YYMM-001 形式status/channel/subject は pgEnum。property(既設設備含む)と roles は jsonb。agency_id は代理店マスタ(event_companies)への FK
deal_status_historyuuid追記専用。from/to/at/by/reason。新規登録時に from=null → アポ の1行を必ず持つ
id_counterskey汎用採番カウンタ(household / person / deal:YYMM)。詳細は下記

論理削除は全テーブル deleted_at。RLS は有効化のみ(行レベル認可はアプリ側=既存テーブルと同じ方針)。

  • Deal.agency は自由文字列 → agencyId FK。crm_next には代理店マスタが実DB化されているため。卸案件とのクロスリンク(代理店名の突合)は第1弾では未実装。
  • 物件(Property)は商談ごとの jsonb(設計どおり値オブジェクト)。
  • 採番は id_counters の atomic upsert(INSERT ... ON CONFLICT DO UPDATE SET seq = seq + n RETURNING)。既存マスタの「最大値スキャン+再試行」は同時登録に弱く、しかも drizzle 0.45 が失敗クエリを DrizzleQueryError で包むため error.code を見る再試行判定が効いていない(世帯・人物・商談では使わない。既存マスタ側は server/pgError.pgErrorCode への置換が後続課題)。初回はテーブル内の既存 ID の最大値を種にするので、データ移行で採番済み ID を投入した後でも衝突しない。
  • ステータス変更は楽観ロック。画面が読んだ時点のステータスを更新の述語に含め、他の操作で先に変わっていれば「再読み込みしてください」で中断し、誤った from の履歴を積まない。
  • 世帯削除と商談作成の競合/人物削除と応対者設定の競合は世帯行の FOR UPDATE で直列化(商談側は世帯ロック取得後に同一トランザクションで人物の所属を確認)。削除済み世帯への商談作成、商談が残る世帯の削除、商談から参照中の人物の削除はいずれもブロック。
  • 失注・クーリングオフのみ理由必須。クーリングオフは成約/工事/完了からのみ。成約時に closeDate 未設定なら当日を自動設定(契約後チェック等の自動生成は未実装)。
  • 重複警告は止めない(同姓名+同じ携帯/固定で警告表示のみ)。名寄せ(統合)は後続。
  • 書き込みは社内スタッフ(hq / ouver_admin / staff)のみ(requireInternalStaff)。
  • 顧客(世帯・人物)の読み取りも社内スタッフのみ。世帯は代理店ごとの所属を持たず「代理店に見せる範囲」を定義できないため、第1弾では fail-closed にした(クエリ側で0件/ページ側で notFound の多層防御)。
  • 商談一覧は代理店ロールなら自社(agencyId 一致)のみ。商談詳細・新規も同じ判定。
  • /customers(一覧・世帯ドロワー=人物の複数行・CSV出力)、/customers/[id](基本情報/人物/商談タブ)
  • /deals(一覧・ステータス/チャネル絞り込み・クイック変更は アポ/前確/商談/工事/保留 のみ)、/deals/new、/deals/[id](次へ進める・保留/失注/クーリングオフ・履歴タイムライン・部分更新の編集セクション)
  • 一覧の既定順は世帯ID降順/商談は作成日降順(updatedAt の露出は未対応)。

商材明細(DealItem)・工程マイルストーン・案件タスク(前確/太陽光調査設計)・活動ログ・契約後チェック・施工・決済・発注・申請・入金予定・カンバンUI・名寄せ・催事シフトからのアポ取り込み導線。

  • 保留案件のリマインド通知経路(チャット/Slack連携の方式)
  • 催事場所(venue)の文字列管理から催事マスタFKへの置き換え時期
  • タスク種別(前確/太陽光調査設計の2種)の拡張範囲。施工・申請系タスクとの統合可否
  • 10kW超・法人申請における必要書類差分の反映
  • 同一世帯への再アプローチ(別チャネルからの重複流入)の名寄せルール
  • 案件ロール(Roles)・タスク担当(DealTask.assignee)のユーザーFK化の時期・移行方法