催事運営
位置づけ: 範囲外(後続・時期未定)。2026-09-09 決定で案1コアから外れ、実装は削除済み
催事運営は、住宅設備商社が実施する店舗催事における代理店シフト管理、現場の開始・終了報告、シフト状態の自動判定、商談CRMとの突合を扱うドメインである。催事店舗マスタ・代理店(販売店)マスタ・催事シフト・開始報告・終了報告の5要素で構成する。
本ページの記述は試作実装(crm_app/app/src/lib/events.ts、crm_app/app/src/lib/permissions.ts、crm_app/app/src/features/crm/EventShiftDetail.tsx。/events として先行取り込み済みの event-shift-system)を正とし、フィールド名・enum・分岐条件をそのまま反映する。ただし2026-08-19定例会で確定した仕様のうち試作実装にまだ反映されていないものは、各節に「(確定仕様。現行試作との差分)」等と明記したうえで追記している。試作実装の記述と確定仕様の記述が矛盾する箇所は、後者(確定仕様)を正とする。
シフトの状態は保存せず、開始報告・終了報告の有無と基準時刻(開始時刻・終了時刻)から都度導出する。催事と商談CRMは外部キーで直結せず、店舗名・日付の一致による疎結合の突合で対応づける。
2026-08-19 定例会での確定事項(本ページに反映済み): 店舗マスタ・代理店マスタの必須項目拡張とマスタ化・CSV一括登録、代理店ロールのログイン(先行に含める)、希望店舗ヒアリング〜自動マッチングによるシフト割当の叩き生成、開始報告への当日メンバー名追加、チェック項目の編集・詳細説明・系列別出し分け、終了報告合計の自動計算、部門のログインアカウント自動導出、通知(前日リマインド・終了報告未提出・アポ数乖離アラート)。あわせて、催事アポ→商談登録の突合・商談化導線(試作の「CRM連携カード」)は先行スコープ外と確定した。詳細は各節および定例会ログを参照。
2026-08-20 是正(本ページに反映済み): クライアントの言う「カテゴリ」は系列(StoreChain)と同一概念(呼称の違い)であると判明したため、別軸として設けていた店舗カテゴリマスタ(StoreCategory)は撤去し、店舗分類は系列1軸に統合した。エリアは自由文字列ではなく47都道府県の固定選択(lib/prefectures.ts の PREFECTURES)に統一し、マスタ化はしない。あわせて、朝礼チェック項目に詳細説明(description)を追加し、系列ごとに独自のチェックセットを持てるようにした(系列に独自セットが無ければ本部基本セットへフォールバック。resolveStartChecks)。希望店舗ヒアリングは対象月の俯瞰(代理店×日付マトリクス)・TSV一括登録・VEによる代理店の希望修正を追加し、催事シフト一覧は未登録アポ件数を状態列から独立させ、各セクションに表示上限20件+「もっと見る」を追加した。詳細は各節を参照。
CSV/TSV一括取込の全マスタ展開(実装済み・試作crm_app): 代理店マスタで先行実装したTSV貼付一括取込を汎用キット(lib/csvImport.ts の makeTsvParser/normalizeHeader)として切り出し、代理店・店舗・系列・シフトの4マスタへ展開済みである(店舗カテゴリマスタは2026-08-20是正で系列に統合し撤去したため対象外)。いずれも「貼付→プレビュー(件数・確認事項)→確定」という同一の操作感(MasterImportSheet)で一括登録できる。詳細は各節および「CSV/TSV一括取込キット(実装済み)」節を参照。
ValueOSとの関係
Section titled “ValueOSとの関係”催事運営はValueOS(商社側実体)に該当する機能を持たない。ValueOSは案件登録(受注)→発注→納品→請求→回収/支払→3社間決済という契約後(後段)の商社ERPであり、テーブル構成(cases / case_products / orders / invoices / settlements / three_party_money_requests、および商品・仕入価格・販売価格・仕入先系マスタ)はすべて受注後の業務を対象とする。店舗・代理店・シフト・朝礼報告・現場ファネル報告に相当するテーブルは存在せず、コード全体(event / 催事 / shift を対象とした検索)にも該当箇所はない。ナビゲーション(AppSidebar.tsx)も「業務(全案件/発注管理/納品管理/回収/支払管理/入金/請求)」「マスタ(販売店/施工店/メーカー/シリーズ/商品/パッケージ/仕入価格/販売価格/仕入先)」「設定」の構成のみで、催事に相当する画面を持たない。
催事は契約前(集客・現場運営)のドメインであり、契約後を扱うValueOSの守備範囲外にある。したがって本ページが定義する催事店舗マスタ・代理店マスタ・催事シフト・開始報告・終了報告はCRM固有の機能として維持し、ValueOS準拠(データモデル突合・機能移植)の対象から除外する。ValueOSのデータモデル全体像はValueOSデータモデル、突合結果の詳細はValueOSとのギャップを参照。
スコープ/対象
Section titled “スコープ/対象”対象は次のとおり。
- 催事店舗マスタ、代理店(販売店)マスタ、系列マスタ(いずれもCSV/TSV一括登録に対応済み。店舗分類は系列1軸に統合しており、店舗カテゴリマスタは2026-08-20是正で撤去した)
- 催事シフト(1日×1店舗×1代理店の単位。一括登録(実装済み)+個別修正)
- 希望店舗ヒアリング〜自動マッチング(店舗割当の叩き生成)
- 開始報告(朝礼チェックリスト。チェック項目は編集可能)、終了報告(現場ファネル実績)
- シフト状態の自動判定
- 通知(前日リマインド・終了報告未提出アラート・アポ数乖離アラート)
- 催事と商談CRMの突合(試作実装の記述として残すが、下表のとおり先行スコープからは除外)
- 権限モデル(社内3ロール/代理店2ロール。代理店ロールの外部ログインを含む)
対象外は次のとおり。いずれも後続フェーズで扱う。
| 対象外項目 | 扱う場所 |
|---|---|
| 顧客・商談の管理本体 | CRM基本機能 |
| 催事アポ→商談登録・突合連携(試作の「CRM連携カード」「この催事のアポを商談登録」ボタンを含む。仮登録機能も含めて先行スコープ外) | CRM基本機能 |
| 見積金額・提案金額 | 価格表・見積 |
| 成果KPI(着座率・アポ率・両面率)の集計ダッシュボード | KPI予実 |
| 卸金額・受発注 | 商社(卸)連携 |
終了報告は現場成果(抽選・着座・アポの件数)を一次データとして持つが、売上金額・受注金額・契約件数の入力は持たない。金額系は商談モジュールおよび商社(卸)モジュールの管轄であり、これは意図的な設計上の切り分けである(2026-08-19定例会で再確認。アポになった時点でアポ側・商談側で登録・管理する)。個人スタッフ(代理店所属メンバー)のマスタ化・シフトへの個人アサインも対象外とし、責任者は氏名の文字列入力、人数は数値入力で足りる軽量設計とする。
終了報告の「部門」は入力させず、ログインアカウント(社員の部署)から自動導出する(詳細は「部門の自動導出」節)。また、催事アポの実績とCRM商談登録数の突合・商談化導線(試作の「CRM連携カード」「この催事のアポを商談登録」ボタン、後述の各節に記述)は、2026-08-19定例会で先行スコープ外と確定した。商談CRM本体が後続フェーズであるため、これに連なる突合・仮登録の運用も後続フェーズで扱う。該当箇所は本ページでは試作実装の挙動として説明を残すが、先行リリースでの提供有無は未定である。
データ(エンティティ表)
Section titled “データ(エンティティ表)”催事店舗マスタ(EventStore)
Section titled “催事店舗マスタ(EventStore)”実装済み: 店舗マスタは店名・エリア・系列・電話番号・責任者・メモ(備考)で構成する。エリアは自由文字列ではなく47都道府県の固定選択(lib/prefectures.ts の PREFECTURES が唯一の情報源。件数固定・CRUD不要のためマスタ化はしない)。系列は StoreChain マスタへの参照(seriesId)で選択式のみを正とする。系列マスタは名前のみを持つ軽量マスタである。2026-08-20是正により、別軸として新設していた店舗カテゴリマスタ(StoreCategory)は撤去した。クライアントの言う「カテゴリ」は本来この系列と同一概念(呼称違い)だったため、店舗分類は系列1軸に統合している。朝礼チェック項目の系列別出し分け(後述)もこの seriesId を基準に行う。旧 category(自由文字列)は既存データ・突合との後方互換のためlegacy表示用フィールドとして残置するが、新規登録・編集は seriesId を正とする。電話番号・責任者・メモ(備考)はCRM側から店舗の連絡先を即座に確認できるようにする狙いで追加済みである(2026-08-19定例会)。
TSV一括取込(実装済み・試作crm_app): 店舗マスタはTSV貼付一括取込(lib/storeImport.ts の parseStoreImport、StoreImportSheet)に対応済みである。店舗名・エリア・系列名・電話番号・責任者・メモの列をタブ区切りで貼り付けると、系列名は既存の系列マスタと名前一致で照合してid化する(「系列を選んでから登録する」形ではなく、行ごとに列の名前でマスタを引き当てる方式)。系列名が一致しない場合は seriesId を未設定のまま行自体は取り込み、確認事項として一覧表示する(行はスキップしない)。エリア列は都道府県の固定表記と一致しない値でも取込は止めず確認事項として警告するのみである(強制的な正規化はしない)。系列マスタ自体も、名前列のみのTSV一括取込に対応済みである(parseChainImport、ChainImportSheet)。いずれも共通の汎用取込キット(後述「CSV/TSV一括取込キット」節)を用いる。
| フィールド | 型・入力形式 | 必須 | 説明 |
|---|---|---|---|
id | 自動採番(例: ST01) | 必須 | 主キー |
name | 自由入力 | 必須 | 店舗名。商談側 venue との突合キーになる |
area | 47都道府県の固定選択(PREFECTURE_OPTIONS) | 任意 | lib/prefectures.ts の PREFECTURES が唯一の情報源。マスタ化はしない(表記ゆれ防止・希望エリア照合・マッチングのエリア一致に用いる) |
seriesId(系列) | マスタ参照・選択式(StoreChain.id) | 任意 | 系列・チェーン(カインズ/島忠/ビバホーム等)。系列マスタは名前のみを持つ。絞り込み・朝礼チェック項目の出し分けに使う |
category(legacy) | 自由文字列 | — | 旧項目。seriesId 解決名との後方互換表示用(新規登録・編集は seriesId を正とする) |
| 電話番号 | 自由入力 | 任意 | 店舗の電話番号。CRM側から即時確認する用途 |
| 責任者 | 自由入力(氏名) | 任意 | 店舗側の責任者名 |
| メモ・備考 | 自由入力 | 任意 | 備考 |
代理店マスタ(EventCompany)
Section titled “代理店マスタ(EventCompany)”代理店(催事会社)は別マスタを設けず、代理店(販売店)マスタに一本化する。代理店は催事・訪販の両方を担うため、事業区分は1値で表す。
2026-08-19定例会で確定・実装済み(試作crm_app): 代理店マスタの項目を、加盟店契約「会社情報」シート準拠で大幅に拡張した。現行実装の最小項目 name・contact・business・paymentType・defaultProfitAmount に加え、次の区分で項目群を追加している(AgencyMasterFields.tsx の編集ドロワーで全項目を編集可能)。詳細フィールド一覧(項目名・型・必須区分)はデータモデル: Phase1エンティティ側に定義する。
| 区分 | 内容(要約) |
|---|---|
| 会社基本情報 | 商号・所在地・代表者名・設立年月日など会社としての基本項目 |
| 契約情報 | 加盟店契約の締結日・契約種別・契約期間などの項目 |
| 金融・口座 | 振込先口座・金融機関情報 |
| 連帯保証人 | 連帯保証人の氏名・連絡先等 |
| 書類添付 | 契約書・許認可証等の添付ファイル |
| リレーション | 店舗マスタ・催事シフト・卸案件など他エンティティとの紐付け |
| 運用フラグ | 稼働可否・取引停止等の運用上のフラグ |
いらない項目も一旦入れておく方針(加盟店契約シートの1行目にある項目群をすべて反映)。代理店マスタは次のTSV一括取込により一括登録に対応する。
TSV一括取込(実装済み・試作crm_app): 加盟店契約「会社情報」シートからのタブ区切り貼付(1行目=ヘッダ)を、ヘッダ表記ゆれ(全角/半角・空白・括弧の有無等)に耐えて解析し、パース結果をプレビュー表示したうえで確定操作で一括登録する機能を実装済みである(lib/agencyImport.ts の parseAgencyTsv、AgencyImportSheet)。貼付データはブラウザ内でのみ保持しサーバへは送信しない。ただし実データ(実在の会社名・代表者名・口座・連帯保証人等の個人情報を含む)はリポジトリや公開モックには一切保存せず、Cloudflare Access等でアクセス制限をかけた環境での取込運用を前提とする(公開URLに実PIIを載せないため)。現状crm_appのモックに投入されているシードデータはすべて架空のダミーである。
代理店マスタのこのTSV一括取込は、店舗・系列・シフトの各マスタへ展開する際の下敷きとして汎用キット(lib/csvImport.ts)に一般化済みである。AgencyImportSheet 自体も汎用ドロワー MasterImportSheet を薄くラップする実装に移行しているが、挙動(ヘッダ表記ゆれ吸収・プレビュー・確定操作)は従来と不変である。汎用キットの詳細は後述「CSV/TSV一括取込キット(実装済み)」節を参照。
催事シフト(EventShift)
Section titled “催事シフト(EventShift)”粒度は「1日×1店舗×1代理店」。同一店舗・同日に複数代理店を入れる場合はシフトを分ける。2026-08-19定例会で、同一店舗・同日に複数催事/複数代理店を入れる運用は「ない」と確定した(1店舗1催事の前提でよい。自社が代理店枠に入るケースも同様に1店舗1催事として扱う)。
割当対象は「代理店+責任者名+人数+店舗」であり、個人スタッフ単位のマスタは不要(責任者はユーザーに紐付けない)と確定している。シフト作成はVE(本部)側のみが行い、代理店は登録しない。承認フローも不要である(現行試作と同様)。
2026-08-19定例会で確定・実装済み(試作crm_app): シフトは一括登録に対応し、登録後の個別修正も可能とする(履歴保持は不要)。新規シフト作成(NewShiftSheet)による1件ずつの登録に加え、TSV貼付一括取込(lib/shiftImport.ts の parseShiftImport、ShiftImportSheet)を実装済みである。貼付行は「日付・店舗名・代理店名・開始時刻・終了時刻・責任者・人数・備考」で構成し(代理店ごとに絞り込んで登録する形ではなく、行ごとに店舗名・代理店名の列を持つ)、店舗名・代理店名は既存の店舗マスタ・代理店マスタと完全一致で照合してid解決する。店舗・系列の取込(前掲)と異なり、店舗名・代理店名は EventShift の必須フィールドであるため、解決できない行はセルを未設定にせず行ごとスキップし、確認事項に理由を記録する。ID採番は NewShiftSheet と同じ newShiftId()(日付ベースの連番)を用いる。
| フィールド | 型・入力形式 | 必須 | 説明 |
|---|---|---|---|
id | 自動採番(S-MMDD-NN 形式) | 必須 | 主キー。同日内の既存IDの最大連番+1 |
date | YYYY-MM-DD | 必須 | 催事日 |
storeId | 店舗マスタ参照 | 必須 | 実施店舗 |
companyId | 代理店マスタ参照 | 必須 | 担当代理店 |
startTime / endTime | HH:MM | 必須 | シフト時間帯 |
plannedLeader | 自由入力(氏名) | 任意 | 予定責任者 |
staffCount | 数値 | 必須 | 人数 |
note | 自由入力 | 任意 | メモ |
startReport | StartReport | 未提出時 undefined | 開始報告 |
endReport | EndReport | 未提出時 undefined | 終了報告 |
開始報告(StartReport)
Section titled “開始報告(StartReport)”2026-08-19定例会で確定(未実装。現行試作との差分): 開始報告の必須項目に「当日入るメンバー名」を追加する。文字列入力で、担当者全員分を書けるようにする(個人単位のマスタ化は行わない)。量販店側から「現在誰が入っているか開示してほしい」という要請があるための対応。
| フィールド | 型・入力形式 | 必須 | 説明 |
|---|---|---|---|
leader | 自由入力(氏名) | 必須 | 当日責任者。plannedLeader とは別フィールドで記録し、予定と実績の差異を残す |
emergencyContact | 自由入力(電話番号) | 必須 | 緊急連絡先 |
| メンバー名(新規) | 自由入力(文字列) | 必須(確定仕様。現行試作には項目なし) | 当日入るメンバー全員の氏名。量販店からの開示要請への対応 |
checks | チェック項目id→真偽のマップ | 任意 | 朝礼チェックリスト |
comment | 自由入力 | 任意 | コメント |
photos | ReportPhoto{name,url} の配列 | 任意 | 現場写真 |
at | ISO日時 | 自動記録 | 提出日時 |
朝礼チェックリストはハードコードせず、マスタ(useEvents の startChecks。本部の基本セット)から与える。各項目は StartCheckItem { id, label, description? } で持ち、詳細説明(description)をラベル下に補足表示する(実装済み)。既定値は次の7項目(DEFAULT_START_CHECKS。いずれも詳細説明つき)。
| id | 項目 |
|---|---|
greeting | 挨拶・声かけ |
mind | 身だしなみ・気持ちの準備 |
booth | ブース設営の確認 |
backyard | バックヤード整理 |
private | 私語・スマホのルール確認 |
rule | 店舗ルールの確認 |
judge | 判断に迷ったら責任者へ |
チェック項目は頻繁に変わる・追加されるため、2026-08-19定例会で運用ルールを次のとおり確定し、いずれも実装済みである。
- チェック項目は編集可能とする(
StartChecksSheet)。 - 編集は履歴を持たず、編集以降のシフトに対して変わったチェック項目が適用される方式とする。
- 項目ごとに詳細説明(
description)を表示する。 - 系列(チェーン)ごとにチェックセットを持てる。
useEventsのseriesChecks(Record<seriesId, StartCheckItem[]>)に系列専用セットを持ち、本部が用意する基本セット(startChecks)を、系列に独自セットが無い場合のフォールバックとする。開始報告に表示する項目は純関数resolveStartChecks(store, startChecks, seriesChecks)(lib/events.ts)が唯一の解決元で、店舗のseriesIdに対応する専用セットがあればそれを、無ければ本部基本セットを返す。2026-08-20是正により、出し分けの基準は「店舗カテゴリ」ではなく系列である(カテゴリと系列は同一概念(呼称違い)だったため、別軸だった店舗カテゴリマスタは撤去し系列に統合した)。StartChecksSheetは「本部・基本セット」「系列別セット」の2タブでこれらを編集する。
終了報告(EndReport)
Section titled “終了報告(EndReport)”現場ファネル3段階(抽選→着座→アポ)の実績を、各段階とも FunnelCount{total, both, single}(合計/両面提案/片面提案の内訳)で記録する。ファネルの3段階構成は2026-08-19定例会で確定した(実態はより細かい段階があるが、先行リリースではこの3段階の枠で足りるとされた)。両面=夫婦揃っている状態、片面=どちらか一方の来場を指し、両面1件・片面2件なら合計は3件(両面を2件とダブルカウントしない)。
2026-08-19定例会で確定(未実装。現行試作との差分): total(合計)は自動計算とし、手入力を廃止する。現行試作は total / both / single をいずれも独立した数値入力としており、内訳との整合バリデーションが無い(後述「エッジケース」参照)。確定仕様ではこの課題を、合計を both + single から自動算出する方式に変更することで解消する。
| フィールド | 型・入力形式 | 必須 | 説明 |
|---|---|---|---|
department | ログインアカウントの所属部署から自動導出(確定仕様。現行試作は自由入力) | 任意(自動導出のため通常入力不要) | 部門(住設 等)。詳細は「部門の自動導出」節 |
lottery | FunnelCount(total は自動計算・確定仕様) | 必須 | 抽選:ブースでの抽選参加者数 |
seated | FunnelCount(total は自動計算・確定仕様) | 必須 | 着座:商談ブースへの着座数 |
appointments | FunnelCount(total は自動計算・確定仕様) | 必須 | アポ:後日商談アポ獲得数 |
photos | ReportPhoto{name,url} の配列 | 任意 | 現場写真 |
at | ISO日時 | 自動記録 | 提出日時 |
終了報告に売上金額・受注金額・契約件数は入れない(2026-08-19定例会で再確認)。アポになった案件は商談(アポ・商談)側で金額・件数を管理する。
CSV/TSV一括取込キット(実装済み・試作crm_app)
Section titled “CSV/TSV一括取込キット(実装済み・試作crm_app)”2026-08-19定例会の「マスタ系は全部一括登録できるように・シフトも一括(代理店ごと)」という要望を受け、代理店マスタで先行実装していたTSV貼付一括取込を汎用キットに一般化し、代理店・店舗・系列・シフトの4マスタへ展開済みである(店舗カテゴリマスタは2026-08-20是正で系列に統合し撤去したため対象外)。
- パーサ層(
lib/csvImport.ts):makeTsvParserに列マッピング(ヘッダ別名→フィールドキーのaliases、必要なら値変換のvalueParsers、必須列のrequiredKey)を渡すだけで各マスタ用パーサを組み立てる。ヘッダの表記ゆれ(全角/半角・空白・括弧の有無等)はnormalizeHeaderで吸収する(NFKC正規化+空白・括弧・中黒・スラッシュ・ハイフン除去)。区切り文字はヘッダ行にタブがあればタブ優先、無ければカンマにフォールバックする(表計算ソフトからの貼付が主用途のため)。対応列が見つからないヘッダ・必須列が空の行はスキップし、理由を確認事項として記録しつつ取込全体は継続する。 - UI層(
MasterImportSheet。旧AgencyImportSheetを一般化): 各マスタはparse(上記パーサ)とpreviewColumns(プレビュー列定義)を渡すだけで、「貼付→プレビュー(件数・確認事項)→確定」という同一の操作感のドロワーになる。貼付データはブラウザ内でのみ保持し、サーバへは送信しない。 - 名前→ID解決の方式差: 店舗取込(
lib/storeImport.tsのparseStoreImport)は系列名を既存の系列マスタと名前一致で解決し、一致しなければ当該セルを未設定のまま行自体は取り込む(任意項目のため)。シフト取込(lib/shiftImport.tsのparseShiftImport)は店舗名・代理店名を同様に名前一致で解決するが、両者ともEventShiftの必須フィールドであるため、解決できない行はセルを未設定にせず行ごとスキップする。系列マスタ自身の取込(parseChainImport)は名前列のみを持つ単純な構成である。 - 実データ(実在の店舗名・代理店の会社情報等)はリポジトリ・公開モックには保存せず、Cloudflare Access等でアクセス制限をかけた環境での取込運用を前提とする。現状crm_appのモックに投入されているシードデータはすべて架空のダミーである。
| 画面 | 内容 |
|---|---|
催事シフト一覧(/events) | 本日/今後/過去の3セクションに仕分けて一覧表示。検索(店舗・代理店・責任者)に加え、月別フィルタ(シフト日付の年月=YYYY-MM を実データから自動生成・複数選択)・状態・店舗で絞り込む(crm_next 実装。2026-09)。サマリーチップ(本日の催事・開始済み・未開始・終了未報告・未登録アポ)を表示。未開始・終了未報告・未登録アポは該当0件のとき非表示。各セクションは初期表示20件(SECTION_LIMIT、EventsPageParts.tsx)で、超過分は「もっと見る」ボタンで20件ずつ追加読み込みする(本番はサーバー側ページング/期間フィルタに置き換え予定)。終了報告アポ数とCRM登録数の乖離は、PC表では状態列とは独立した「未登録」列に件数を表示し(0件は「—」)、モバイルカードでは状態Pillの隣に「未登録 N」バッジ(GapBadge)を表示する(乖離が0件の行は表示しない。詳細は後述「通知」節)。代理店ロールは自社分のみ表示(eventScopeAgency によるクライアント側フィルタ) |
新規シフト作成(NewShiftSheet、一覧の「新規シフト」ボタン) | 日付・店舗・代理店・時間帯・責任者(任意)・スタッフ人数・備考を入力するドロワー。canManageShifts(role) が真のときのみボタンを表示。送信すると newShiftId() でID採番→addShift()→シフト詳細へ遷移 |
シフト一括登録(ShiftImportSheet/crm_next は ShiftBulkImport。実装済み) | TSV貼付(日付・店舗名・代理店名・時間帯・責任者・人数・備考)でシフトをまとめて登録する機能。店舗名・代理店名は既存マスタと完全一致で解決し、解決できない行はスキップする。crm_next では実DB化(Server Action bulkCreateShifts=同一 日×店舗×代理店 の重複を除外し ID サーバー採番。行単位で検証し不正行だけスキップ=1行の不正で全件失敗させない)。登録後の個別修正も可能(履歴保持は不要) |
朝礼チェック項目編集(StartChecksSheet、一覧のボタン) | 開始報告の朝礼チェックリスト項目を編集するドロワー。「本部・基本セット」「系列別セット」の2タブで構成する(実装済み)。本部・基本セットタブは startChecks を追加・編集(ラベル・詳細説明description)・削除する。系列別セットタブは系列を選択し、その系列専用のチェックセット(seriesChecks[seriesId])を作成・編集・削除できる。専用セットが無い系列は「デフォルト(本部基本)適用中」と表示され、本部基本セットの内容を参照のみで確認できる。専用セット作成時は本部基本セットを複製した初期値になる。表示条件は新規シフト作成と同じ canManageShifts(role) |
店舗マスタ・代理店マスタ・系列マスタのTSV一括登録(StoreImportSheet・AgencyImportSheet・ChainImportSheet。実装済み) | いずれもTSV貼付→プレビュー→確定の同一操作感。店舗マスタは系列名を既存マスタと名前一致で解決する(選択式で系列を選んでから登録する形ではない) |
希望店舗ヒアリング〜自動マッチング(/events/matching。実装済み。詳細は「希望店舗ヒアリング〜自動マッチング」節) | (1)対象月の俯瞰(代理店×日付のマトリクスで対象月の希望提出状況を一望。TSV一括登録、行からVEによる代理店の希望修正に対応) (2)取れた枠登録(AvailableSlot=日×店舗を単発登録・一括登録の2方式で登録。1枠=1代理店のためレーン数の概念は撤去済み) (3)マッチング実行(「叩きを生成」でautoMatch()を実行→ドラフト表で行ごとに手動差替可能→「シフトに反映」でEventShiftを一括生成)の3セクション+代理店自身の希望提出フォームで構成する。管理者/営業/事務(BO)は(1)〜(3)を、代理店(管理者ロール)は希望提出フォームのみを表示する(代理店(メンバー)はナビ非表示・アクセス不可) |
催事シフト詳細(/events/$shiftId) | 店舗・時間帯・代理店・人数・責任者の帯表示、開始報告カード、終了報告カード、CRM連携カード、この販売店の卸案件カード |
| 開始報告カード | 未提出時はチェックリスト・責任者・緊急連絡先・当日メンバー名(確定仕様・未実装)・コメント・写真の入力フォーム。提出済み時は読み取り専用表示 |
| 終了報告カード | 未提出時はファネル3段階×2区分(両面・片面)の数値入力フォームで、合計は自動計算とする(確定仕様。現行試作は3区分=合計・両面・片面をいずれも数値入力)。提出済み時は着座率・アポ率を含む読み取り専用表示 |
| CRM連携カード(試作実装。2026-08-19定例会で先行スコープ外と確定) | 終了報告提出後に表示。報告済みアポ数とCRM登録件数の差分を警告表示するとともに、「この催事のアポを商談登録」ボタンから来店アポをその場で商談化できる。商談CRM本体が後続フェーズであるため、この連携機能も後続フェーズ扱いとする |
この販売店の卸案件カード(DealerWholesaleCard) | 代理店名で卸案件を突合し、件数を表示して /wholesale へ遷移するボタンを出す。0件時は非表示 |
提出済みの開始報告・終了報告は読み取り専用表示のみであり、再編集用のUIは持たない。シフト自体の編集UI(日時・店舗・代理店の変更)も試作には無く、useEvents.updateShift() はストア関数として存在するのみで呼び出し元を持たない。
現場入力はスマホ主体とし、PCでも利用できるようにする(UIはPC基準のレイアウトも用意する)。オフライン一時保存は不要(対象店舗はすべてオンライン接続前提のため)。催事関連の帳票印刷(シフト表等)は不要である。見積書・発注書のPDF化は別モジュール(価格表・見積・商社(卸)連携)で扱う(いずれも2026-08-19定例会で確定)。
業務ルール・ワークフロー
Section titled “業務ルール・ワークフロー”シフト状態の自動判定
Section titled “シフト状態の自動判定”shiftStatus() は保存済みの状態を持たず、startReport / endReport の有無と基準時刻(startTime / endTime)から都度導出する。判定は now >= startAt / now >= endAt の時刻比較であり、猶予時間は設けない。基準時刻を過ぎた瞬間に状態が切り替わる。
| ステータス | 現在時刻の条件 | 開始報告 | 終了報告 | 一覧での表示トーン |
|---|---|---|---|---|
| 終了済み | 問わない | — | あり | secondary(グレー) |
| 終了未報告 | 終了時刻を過ぎた | あり | なし | 警告色 |
| 開始済み | 終了時刻前 | あり | なし | 進行中トーン |
| 未開始 | 開始時刻を過ぎた | なし | なし | 警告色(赤系) |
| 予定 | 開始時刻前 | なし | なし | secondary(標準) |
終了報告があれば時刻に関わらず無条件で「終了済み」となる。終了時刻前の早期終了報告でも成立する。現行試作は異常ステータス(未開始・終了未報告)の自動通知機能を持たず、一覧の目視運用で検知する。2026-08-19定例会で終了報告未提出アラート等の通知機能を追加することが確定した(未実装。詳細は後述「通知」節)。
shiftStatus() の判定順序(実装の分岐順)を状態遷移として図示する。
stateDiagram-v2
[*] --> 予定
予定 --> 未開始: now >= startAt かつ 開始報告なし
予定 --> 開始済み: 開始報告 提出(now < endAt)
未開始 --> 開始済み: 開始報告 提出(now < endAt)
未開始 --> 終了未報告: 開始報告 提出(now >= endAt)
開始済み --> 終了未報告: now >= endAt かつ 終了報告なし
開始済み --> 終了済み: 終了報告 提出
終了未報告 --> 終了済み: 終了報告 提出
予定 --> 終了済み: 終了報告 提出(開始報告の有無を問わない)
未開始 --> 終了済み: 終了報告 提出(開始報告の有無を問わない)
終了済み --> [*]
図中の「予定→終了済み」「未開始→終了済み」は、shiftStatus() が ended(終了報告の有無)を最優先で判定するために生じる分岐であり、開始報告を経ずに終了報告だけが提出された場合も成立する。UI・ストア側にこの順序を強制するバリデーションは無いため、運用上は開始報告なしの終了報告提出をブロックしない(画面側では終了報告カードの入力欄自体を「開始報告未提出時は非表示」にすることで通常操作では起こらないよう誘導しているが、データ層では防いでいない)。
希望店舗ヒアリング〜自動マッチング(新規業務フロー・実装済み: /events/matching)
Section titled “希望店舗ヒアリング〜自動マッチング(新規業務フロー・実装済み: /events/matching)”2026-08-19定例会で確定した新規業務フローで、試作実装(crm_app)に実装済みである(lib/events.ts・lib/eventMatching.ts・features/crm/EventMatchingPage.tsx)。VEが量販店本部と場所取りを取り決め、取れた枠を代理店に割り振る運用を、システム内での希望提出と自動マッチングに置き換える。
現状(before): LINEでの希望吸い上げ→スプレッドシートでの手作業マッチング→正本のシフト表を作って代理店に投げる、という運用である。取れた店舗の連絡も現状はLINEグループでのスクリーンショット共有が中心。
実装済みの運用(after):
- 店舗を確保できる期間の制約上、基本的に2ヶ月先の店舗しか決められない。そのため「今月に2ヶ月先(例: 今月=8月なら10月)の希望を回収する」というサイクルで運用する(実装済み。
HOPE_TARGET_MONTH)。 - 毎週月曜に代理店へ希望収集の通知を送る(未実装。前掲「通知」節参照。メール/LINE/Slackでの自動送信は別途実装が必要)。
- 代理店(管理者ロール=「代理店」)が
/events/matchingの希望提出フォームからシステム内で希望を提出する(実装済み)。ヒアリング項目は次の3点。- 日別の稼働可否(その日は稼働できるか)
- 希望店舗(最大3・優先順位付き。第1〜第3希望を重複選択不可の選択式で入力。レスポンシブSelect=モバイルはnative select、PCは検索コンボボックス)
- 希望店舗に漏れた場合のエリア指定(複数選択可。レスポンシブ複数選択=選択済みはチップ表示)
- 「下書き保存」と「提出する」を分けて持ち、自動マッチングの対象は「提出済」のみ(下書きは対象外)
- VE側は「対象月の俯瞰」(代理店×日付のマトリクス、
HopeOverviewSection)で全代理店の提出状況を一望できる(実装済み)。行の編集ボタンから任意の代理店の希望をその場で修正でき(代理店に代わってVEが入力・修正する運用に対応)、電話・メール等で口頭収集した希望はTSV貼付一括登録(HopeImportSheet)でまとめて反映できる。同一代理店×対象月の再取込は貼付内容で丸ごと置換する(bulkUpsertHopeSubmissions。自然キーH-{companyId}-{targetMonth}一致で置換、無ければ追加)。 - VE側が量販店本部と交渉して取れた枠(
AvailableSlot)を同画面に登録する(実装済み。単発登録・一括登録の2方式)。 - 「叩きを生成」ボタンで自動マッチング(
autoMatch())を実行し、店舗割当の叩き(MatchDraft)を生成する(実装済み)。
自動マッチングは「叩きを作る」機能であり、最終確定は人手判断を残す。叩きの各行(ドラフト表)は担当者が手動で割当代理店を差し替えられ(「未割当」を含む選択式。差し替えた行にはUI上「(手動)」表示が付く)、「シフトに反映」ボタンを押すまでシフト(EventShift)は生成・確定されない。
画面構成(/events/matching。EventMatchingPage.tsx):
| セクション | 内容 |
|---|---|
(1) 対象月の俯瞰(HopeOverviewSection) | 代理店×日付のマトリクスで対象月の希望提出状況を一望する。行=代理店(提出状況バッジ=未提出/下書き/提出済)、列=その対象月の全提出に現れる日付の和集合(横スクロール+代理店列は固定表示)。セルは稼働可否と希望店舗数(○件数/×/未提出は—)を表示。行の編集ボタンから任意の代理店の希望を修正できる(HopeSubmitSectionを対象代理店・対象月で起動)。TSV一括登録ボタン(HopeImportSheet)を併設。管理者/営業/事務(BO)のみ表示 |
(2) 取れた枠登録(SlotSection) | AvailableSlot(日×店舗。1枠=1代理店のためレーン数の概念は撤去済み)を単発登録(AddSlotSheet:日付・店舗・メモ)または一括登録(BulkSlotSheet:1行1件のテキスト貼付、日付,店舗ID または店舗名形式。解釈不能な行はスキップしスキップ件数をトースト表示)で登録。管理者/営業/事務(BO)のみ表示 |
(3) マッチング実行(MatchingSection) | 「叩きを生成」ボタンでautoMatch()を実行し、合計枠数・割当済み・未割当・あぶれた希望の件数をチップ表示。ドラフト表(DraftTable)で日付・店舗・割当代理店(選択式で手動差替可)・区分(第1希望/第2希望/第3希望/エリア/未割当)を確認し、「シフトに反映」ボタンで確定。あぶれた希望一覧(UnmetHopesList)も同セクションに表示。管理者/営業/事務(BO)のみ表示 |
希望提出フォーム(HopeSubmitSection) | 代理店(管理者ロール=「代理店」)のみ表示。対象月の日別に稼働可否・第1〜第3希望店舗(レスポンシブSelect)・希望エリア(レスポンシブ複数選択・チップ表示)を入力し、「下書き保存」または「提出する」で送信。(1)の俯瞰からVEが起動した場合も同じフォームを対象代理店・対象月で再利用する。代理店(メンバー)はナビゲーション自体に本画面へのリンクを持たず、アクセス不可 |
アルゴリズム(autoMatch()。lib/eventMatching.ts):
決定的貪欲法の純関数で、Date.now()・乱数を使わず、同一入力からは常に同一出力を返す。
- 対象は提出済み(
status === "提出済")のHopeSubmissionのみ。下書きは候補にしない。 - 登録済みの
AvailableSlot(枠=日×店舗。1枠=1代理店。2026-08-19定例会Q9確定)を日付昇順→店舗ID昇順の決定的順序で処理する。 - 各枠について、その日その店舗を希望した未割当の代理店から、希望順位(第1希望→第2希望→第3希望)を優先し、同順位内は提出時刻(
submittedAt)昇順→同時刻ならcompanyId昇順で1社を選ぶ。 - 直接の店舗希望で埋まらない枠は、同日にエリア希望(
hopeAreas)が枠の店舗エリアと一致し未割当の代理店から選ぶ(区分=「エリア」、同じく提出時刻→companyIdの順で1社選出)。 - それでも埋まらない枠は区分「未割当」のドラフト行になる。
- 代理店は1日1枠まで(同日に複数枠へは割り当てない)。
- 稼働可・希望を出したのにその日どこにも割り当たらなかった代理店(あぶれた希望)は
UnmetHopeに集計する。理由は「店舗満枠」(希望店舗/エリアに枠自体はあったが埋まった)と「枠なし」(VEが希望店舗/エリアに枠を確保できていない)の2種で区別する。
「シフトに反映」実行時は、代理店が割り当たっているMatchDraft行のみを対象に、EventShiftを既定値(開始時刻10:00・終了時刻18:00・人数1名・責任者未定)で1行ずつ生成する。同一日・同一店舗・同一代理店のEventShiftが既に存在する行はスキップする(重複生成を防止。エラー表示はせずスキップ件数をトースト表示)。生成後の初期値(時間帯・人数)はシフト詳細で個別に編集する運用を想定する。
エンティティ(詳細フィールドはデータモデル: Phase1エンティティを参照):
HopeSubmission: 代理店1社・対象月1件の希望提出。companyId・targetMonth(YYYY-MM。2ヶ月先)・entries[](date・available・hopeStoreIds=最大3・優先順・hopeAreas)・status(下書き/提出済)で構成する。自然キーid = "H-{companyId}-{targetMonth}"のため、TSV一括登録・俯瞰からのVE修正による再提出はいずれも同じIDのレコードを置換するAvailableSlot: VEが確保した枠。date・storeId・note。1枠=1日×1店舗=1代理店(2026-08-19定例会Q9確定)で、旧lanes(レーン数)フィールドは過剰モデルのため撤廃済みMatchDraft: マッチング結果1行=1枠。date・storeId・companyId(未割当時は無し)・source(第1〜第3希望/エリア/未割当)。永続化せずautoMatch()の呼び出しごとに算出するUnmetHope: あぶれた希望。companyId・date・hopeStoreIds・hopeAreas・reason(店舗満枠/枠なし)。MatchDraftと同様に永続化しない
部門の自動導出
Section titled “部門の自動導出”終了報告の「部門」は入力させず、ログインアカウント(社員の部署)から自動導出する(2026-08-19定例会で確定。未実装)。社員の部署履歴(申請時点でどの部署に所属していたかを紐付けたデータ)を保持し、通常はその履歴から部門を取得する。イレギュラー(特別に別部署扱いとする等)が発生した場合は、自動導出値を上書きできるようにする。代理店側にも部門分けを持たせられるようにする(1代理店の中に自社メンバーと代理店メンバーが混在するケースに対応するため)。部署履歴エンティティの詳細フィールド定義はデータモデル: Phase1エンティティ側に置く。
2026-08-19定例会で、催事運営における通知機能を次のとおり確定した。
- 前日リマインド(未実装)
- 終了報告未提出アラート(未実装)
- 終了報告アポ数と実登録数の乖離アラート(登録漏れ通知)(判定・可視化は実装済み。通知台帳へのルール追加も実装済み。実際のSlack/メール自動送信配線は未実装)
終了報告アポ数と実登録数の乖離アラート(登録漏れ通知): 現場が終了報告で申告したアポ件数(endReport.appointments.total)に対し、入口フォームからCRMへ実際に登録された件数(dealsForShift()の件数)が不足しているケースを「登録漏れ」として検知する機能である。従来は一覧を目視で探して漏れを見つける運用だったため、これを機械的な検知・表示に置き換えた(2026-08-19定例会)。判定はlib/events.tsの純関数appointmentGap(shift, store, deals)(1シフト分。missing = max(0, reported − registered))とshiftsWithAppointmentGap(shifts, stores, deals)(終了報告済みでmissing > 0のシフトのみを抽出)が唯一の情報源であり、次の3箇所の表示はいずれもこの2関数を参照する(値の二重計算・二重管理をしない設計。詳細は前述「催事↔商談の突合」節)。
| 表示箇所 | 内容 |
|---|---|
催事シフト詳細(CrmLinkCard、EventShiftDetail.tsx) | 終了報告提出後、missing > 0のときのみ「未登録 N 件(入口フォームからの登録漏れの可能性)」バナーを表示 |
催事シフト一覧(/events、EventsPage.tsx) | サマリーチップ「未登録アポ」に該当シフト数を表示(0件のとき非表示) |
催事シフト一覧の各行(EventsPageParts.tsx) | PC表は状態列と独立した「未登録」列に件数を表示(0件は「—」)。モバイルカードは状態Pillの隣に「未登録 N」バッジ(GapBadge)を表示し、乖離が0件の行では表示しない |
通知台帳(lib/notifications.ts)にはevent-appointment-gapルールを追加済みである。宛先は#催事・営業DM、タイミングは「終了報告の提出時/日次まとめ」、既定ONである。ただし他の催事系ルール(ev-start-missing・ev-end-missing・ev-daily)と同様、これはルール定義(設定画面のマトリクス表示・ON/OFF切替)にとどまり、実際のSlack・メールへの自動送信配線は無い(実送信が動作する経路はタスク依頼フローと設定画面のテスト送信のみ。詳細はチャット・通知を参照)。
通知手段はメール/LINE/Slackの3系統とし、会社ごとに使う手段を分けられるようにする(普段使うツールが会社によって異なるため。2026-08-19定例会)。本台帳(NotifyDest)はSlack宛先モデルを前提とした定義であり、event-appointment-gapを含め、会社ごとのメール/LINE/Slack振り分けはまだ反映されていない。メールはシンプルな仕様で先行実装してよいが、LINE・Slack連携の詳細仕様、および会社別の手段振り分けの実装方式は確認事項として残る(後述「確認事項」参照)。
開始報告の提出ルール
Section titled “開始報告の提出ルール”全チェック完了は送信の必須条件ではない。未チェックの項目があっても送信ボタンは有効であり、画面には「未チェック N 件(そのまま送信できます)」という警告のみを表示する。送信に必須なのは leader(責任者)と emergencyContact(緊急連絡先)の非空入力のみである。報告時刻(at)は自動記録され、ステータス「開始済み」への遷移トリガーになる。
終了報告の提出ルール
Section titled “終了報告の提出ルール”終了報告は開始報告の提出後にのみ入力可能である。開始報告が未提出の場合、終了報告カードには「開始報告の送信後に入力できます」の案内のみを表示し、入力欄自体を出さない。提出済み表示では、着座率(着座total÷抽選total)とアポ率(アポtotal÷着座total)を自動算出する。分母が0のときは — と表示する。
現行試作は total と both+single の内訳一致を強制するバリデーションを持たず、各セルを独立した数値入力としている。2026-08-19定例会で total は自動計算(both + single)に変更し、手入力欄を廃止することが確定した(未実装)。これにより内訳の入力ミスによる合計の食い違いは仕組み上発生しなくなる想定である。
開始報告・終了報告とも現場写真を添付できる(ReportPhoto{name,url})。試作実装ではブラウザ内で dataURL として保持するモック実装であり、本番実装では Storage の URL に差し替える。
シフトID採番
Section titled “シフトID採番”シフトIDは S-MMDD-NN 形式で自動採番する。同日内に既存するIDの最大連番に1を加えた値を採る。
報告提出からCRM突合までのシーケンス
Section titled “報告提出からCRM突合までのシーケンス”開始報告・終了報告の提出は、いずれも「フォーム入力 → ストア更新 → 画面はストアの現在値を再購読して自動再描画」という単方向フローであり、送信APIの往復や楽観的更新のロールバックは無い(useEvents はローカル永続化のZustandストアであり、サーバー確定を待つ状態を持たない)。
sequenceDiagram
actor Staff as 代理店(メンバー)/代理店
participant Detail as EventShiftDetail
participant Store as useEvents(Zustand・永続化)
participant Status as shiftStatus()
participant Deals as useDeals(商談ストア)
Staff->>Detail: 責任者・緊急連絡先・朝礼チェックを入力
Detail->>Detail: canSubmit = leader かつ emergencyContact が非空
Detail->>Store: submitStartReport(shiftId, report)
Store-->>Detail: shift.startReport 更新(即時反映)
Detail->>Status: shiftStatus(shift, now)
Status-->>Detail: "開始済み"(endAt未到来)
Note over Detail: 終了報告カードの入力欄が開放される
Staff->>Detail: 抽選/着座/アポ(合計・両面・片面)を入力
Detail->>Store: submitEndReport(shiftId, report)
Store-->>Detail: shift.endReport 更新(即時反映)
Detail->>Status: shiftStatus(shift, now)
Status-->>Detail: "終了済み"
Detail->>Deals: dealsForShift(shift, store, deals)
Deals-->>Detail: channel==="催事" ∧ venue===store.name ∧ createdAt===shift.date の Deal[]
Detail->>Detail: missing = endReport.appointments.total − linked.length
Detail-->>Staff: CRM連携カードに「未登録 N 件」を条件付き表示(missing > 0 のときのみ)
催事↔商談の突合(疎結合連携)
Section titled “催事↔商談の突合(疎結合連携)”2026-08-19定例会で確定: 本節(催事↔商談の突合)および次節(催事アポの商談登録ボタン)が記述する連携機能は、商談CRM本体・仮登録機能を含めて先行スコープ外と確定した(今は先行運用なし。従来どおりスプレッドシートで管理する)。以下は試作実装(crm_app)に既に組み込まれている挙動の記述として残すが、先行リリースでの提供有無・提供時期は未定である。
催事と商談は外部キーで直結せず、dealsForShift() による疎結合の突合で対応づける。突合条件は次の3つの完全一致である。
- 商談側
channel === "催事" - 商談側
venue === store.name(店舗マスタのnameと完全一致) - 商談側
createdAt === shift.date(日付の完全一致。期間・前後日は対象外)
疎結合を採用する理由は、第1弾では商談CRM本体が未リリースであり、先行する催事側に商談IDを持たせられない運用開始順序の非対称性による。終了報告の提出後、シフト詳細に「CRM連携」カードが表示され、報告済みアポ数(endReport.appointments.total)とCRM登録件数の差分を「未登録 N 件(入口フォームからの登録漏れの可能性)」として警告表示する。突合キーが店舗名の文字列一致と日付の完全一致に依存するため、店舗マスタ名を正とする入力運用を前提とする。
アポ数と実登録数の乖離検知(実装済み・試作crm_app): 上記の突合ロジックはappointmentGap(shift, store, deals)という純関数(lib/events.ts)に切り出されており、{reported, registered, missing}(reported=終了報告のappointments.total、registered=dealsForShift()の件数、missing = max(0, reported − registered))を返す。催事シフト一覧のサマリーチップ・行バッジ(前掲「催事シフト一覧」画面)も同じくここから切り出したshiftsWithAppointmentGap(shifts, stores, deals)(終了報告済みでmissing > 0のシフトのみを日付降順・シフトID昇順で返す)を用いる。個票のCRM連携カードのバナー・一覧のサマリーチップ・一覧の行バッジ・通知台帳のルール(event-appointment-gap、次節「通知」参照)は、いずれもこの2関数を唯一の判定源とし、乖離件数を二重に計算しない設計になっている。
催事アポの商談登録ボタン(商談化導線)
Section titled “催事アポの商談登録ボタン(商談化導線)”疎結合突合による警告表示に加え、CRM連携カード(EventShiftDetail.tsx の CrmLinkCard)には「この催事のアポを商談登録」ボタンを常設で実装済みである。ボタンは商談側の入口フォーム(NewDealSheet)を initial={{ channel: "催事", venue: store.name }} で起動し、商流「催事」・催事場所=当該店舗名をプリフィルした状態で開く。これにより、催事シフト詳細の画面を離れずに来店アポをそのまま商談(Deal)として正式登録でき、登録された Deal は dealsForShift() の突合条件(channel==="催事" かつ venue===store.name)を確実に満たす(店舗名の手入力による表記ゆれ突合漏れを避けられる)。ボタンは終了報告の提出有無やアポ件数の過不足に関わらず常時表示され、件数差分(未登録 N 件)が0のときも押下できる。商談化された Deal は以後、通常の商談CRM(顧客・商談CRM)のパイプラインで管理される。
催事⇄卸(代理店/販売店名でのクロスリンク)
Section titled “催事⇄卸(代理店/販売店名でのクロスリンク)”試作実装(crm_app)では、催事シフト詳細と卸案件詳細のあいだに代理店(販売店)名を共通キーとした相互参照リンクを実装済みである。催事(前段)と卸(後段)は別軸のまま維持し、外部キーによる連結や軸の統合は行わない。追加するのは参照リンクのみである。
- 催事シフト詳細(
/events/$shiftId、EventShiftDetail.tsxのDealerWholesaleCard): シフトのcompanyIdから代理店マスタのnameを引き、卸案件のdealerフィールドと完全一致する卸案件を抽出して「この販売店の卸案件」件数を表示する。該当が0件のときはカード自体を非表示にする。表示時は「卸案件一覧」ボタンから/wholesaleへ遷移する。 - 卸案件詳細(
/wholesale/$caseId、WholesalePages.tsxのDealerEventsCard): 卸案件のdealerと名称が一致する代理店マスタを検索し、一致する代理店に紐づく「販売店の催事」シフト件数・直近日付(シフトdateの最大値)を表示する。一致する代理店が無ければカードを非表示にする。表示時は「催事一覧」ボタンから/eventsへ遷移する。
突合キーは代理店(販売店)名の完全一致であり、外部キーでの連結ではない。催事↔商談突合と同様、名称の表記ゆれがあれば突合漏れが生じうる。本リンクは相互参照のみであり、催事(前段)と卸(後段)の別軸原則(本ページ冒頭「ValueOSとの関係」参照)は変更しない。
flowchart LR
subgraph EV["催事シフト詳細 /events/$shiftId"]
SC["shift.companyId"] --> CN["代理店マスタ.name"]
end
subgraph WH["卸案件詳細 /wholesale/$caseId"]
CD["case.dealer"]
end
CN -- "完全一致(cases.filter(c.dealer===name))" --> CD
CD -- "完全一致(companies.find(name===dealer))" --> CN
CN -->|"件数>0"| A["「この販売店の卸案件」カード表示 → /wholesale へ"]
CN -->|"件数=0"| A0["カード非表示"]
CD -->|"シフト件数>0"| B["「販売店の催事」カード表示(直近日付=date最大) → /events へ"]
CD -->|"シフト件数=0"| B0["カード非表示"]
両カードとも一覧全体への遷移のみで、個別シフト・個別卸案件への直接ディープリンクは持たない(遷移先で改めて絞り込みが必要)。
permissions.ts 上、催事シフトに関わるロールは全7ロール中、社内3ロール(管理者/営業/事務(BO))と代理店2ロール(代理店=管理者、代理店(メンバー))である。発注・申請担当と施工店は催事に一切関与しない(ナビゲーション非表示)。代理店の両ロールは isAgencyRole() により共通の「代理店ロール」として扱う。
2026-08-19定例会で、代理店ロールのログイン(外部招待・約40社への個別アカウント発行)を先行スコープに含めることが確定した。アカウントはURL方式または個別発行方式のいずれかで代理店ごとに判別する。代理店側の階層は「管理権限者」ロールとその配下の「催事スタッフ」ロールの2階層とし、代理店(管理サイド)=代理店ロール、メンバー=催事スタッフロールとして扱う(本ページの「代理店」「代理店(メンバー)」表記に対応)。ログイン機能自体は現行試作にも存在するが、約40社分のアカウント個別発行運用は未着手。
画面到達可否(NAV_ALLOW)
Section titled “画面到達可否(NAV_ALLOW)”| ロール | /events(一覧・詳細) | /events/stores(店舗マスタ) | /events/companies(代理店マスタ) |
|---|---|---|---|
| 管理者 | 可("all") | 可 | 可 |
| 営業 | 可 | 可 | 可 |
| 事務(BO) | 可 | 可 | 可 |
| 発注・申請担当 | 不可 | 不可 | 不可 |
| 代理店 | 可 | 不可 | 不可 |
| 代理店(メンバー) | 可 | 不可 | 不可 |
| 施工店 | 不可 | 不可 | 不可 |
/events/$shiftId(詳細)は /events 配下として同じ許可判定に含まれ、詳細専用のナビ制御は無い。
| 操作 | 管理者・営業・事務(BO) | 代理店 | 代理店(メンバー) |
|---|---|---|---|
シフト作成(NewShiftSheet) | 可(canManageShifts) | 不可(ボタン非表示) | 不可(ボタン非表示) |
朝礼チェック項目編集(StartChecksSheet) | 可(canManageShifts) | 不可(ボタン非表示) | 不可(ボタン非表示) |
| 自社シフト閲覧 | 可(全件) | 可(eventScopeAgency でスコープ) | 可(eventScopeAgency でスコープ) |
| 他社シフト閲覧(一覧) | 可 | 不可(一覧側フィルタで除外) | 不可(一覧側フィルタで除外) |
| 開始報告・終了報告の提出* | 画面上は可(ガード無し) | 可 | 可 |
| 店舗・代理店マスタ編集 | 可 | 不可 | 不可 |
* EventShiftDetail.tsx は permissions.ts を一切importしておらず、開始報告・終了報告の送信ボタンの表示・活性はロールに関係しない(leader・emergencyContact の非空チェックのみ)。したがって画面上は、社内ロールでシフト詳細URLに到達できれば代理店に代わって代行入力することも技術的には可能であり、これを禁止する仕組みは実装に無い(想定運用として代行入力を許すか禁止するかは未定。確認事項参照)。
外部扱い・情報可視性(代理店に波及する共通ルール)
Section titled “外部扱い・情報可視性(代理店に波及する共通ルール)”代理店(管理者・メンバーとも)は isExternal() により施工店と同じ「外部」区分となり、次の可視性ルールが適用される。これらは案件詳細(/deals/$dealId)など催事以外の画面にも及ぶが、代理店ロールが催事運営から案件へ遷移する動線(CRM連携カードのリンク等)で直接影響する。
| 関数 | 管理者・営業・事務(BO) | 代理店 | 代理店(メンバー) |
|---|---|---|---|
canSeeCost(仕切原価・粗利) | 見える | 見えない | 見えない |
canSeeProposal(ご提案金額) | 見える | 見える | 見えない |
canSeeInternalMemo(運営メモ) | 見える | 見えない | 見えない |
canCreateDeal(案件の起票) | 可 | 可(自社リード起票) | 不可 |
canExport(エクスポート) | 可 | 不可 | 不可 |
isFieldRole(モバイル前提レイアウト) | 営業のみ該当 | 該当 | 該当 |
visibleDealTabs(案件詳細タブ) | 全タブ | overview/items/process/chat/activity | overview/process/chat |
visibleScopes(チャットルーム) | 社内/施工店/代理店 | 代理店のみ | 代理店のみ |
一覧(/events)は eventScopeAgency により代理店の自社分のみにスコープされる。ただし詳細画面(/events/$shiftId、EventShiftDetail.tsx)は shiftId からシフトを直接取得するのみで代理店・会社IDによる絞り込みを行っておらず、他社シフトのURLを直接指定した場合のアクセスを防ぐガードは未実装である。試作はナビゲーション(NAV_ALLOW)と画面・ストア層のデータフィルタで絞り込む実装であり、本番実装では Supabase の API・RLS設計を別途行う。
エッジケース/異常系
Section titled “エッジケース/異常系”- 店舗名の未マップ:
dealsForShift()はvenue === store.nameの完全一致で突合するため、商談側venueの表記が店舗マスタのnameと1文字でも異なると突合0件になる。突合漏れはCRM連携カードの「未登録 N 件」警告として現れるが、原因(表記ゆれか本当の未登録か)は画面上区別できない。 - 代理店名の未マップ(催事⇄卸):
DealerWholesaleCard/DealerEventsCardはいずれも一致0件時にカード自体を非表示にする(エラー表示・空状態メッセージは無い)。代理店マスタと卸案件dealerの名称が食い違っていても、ユーザーには「関連が無い」のか「表記ゆれで拾えていない」のか区別する手掛かりが無い。 - 終了報告が開始報告に先行するデータ: シード・手動操作を問わず
startReport無しでendReportのみ存在するシフトを作れば、shiftStatus()は無条件で「終了済み」を返す(前掲stateDiagram参照)。UIの通常操作では起きないが、ストア(submitEndReport)自体は開始報告の有無をチェックしないため、直接ストアを操作する経路(永続化データの手動編集等)では発生し得る。 totalと内訳(both+single)の不一致: 終了報告の各セルは独立入力で、合計と内訳の整合バリデーションが無い。着座率・アポ率はtotal列のみで算出するため、内訳の入力ミスは指標に影響しない一方、内訳同士の矛盾(例:both + single > total)はそのまま保存・表示される。2026-08-19定例会でtotalを自動計算(both + single)に変更することが確定しており(未実装)、実装後はこの不一致自体が構造上発生しなくなる想定である。- 分母0件の率表示: 抽選
totalが0のときの着座率、着座totalが0のときのアポ率はいずれもnullを経てpct()により—表示になる(0%やエラーにはならない)。 - 写真0枚:
PhotoStripはphotosが未定義または0件のとき何も描画しない(見出しラベルごと非表示)。 - 卸案件・催事シフトいずれかがまだ無い代理店: 新規に追加した代理店(マスタ)は、卸案件・催事シフトどちらの実績も無い間、両カードとも表示されない。導線が無いため、代理店を新規登録した直後に「関連付ける操作」が必要という案内は画面上出ない。
- シフトIDの日跨ぎ生成:
newShiftId()は引数dateのみから月日を切り出す(年をキーに含まない)ため、同じ月日で年をまたぐ既存データがある場合、既存の連番と衝突しうる。試作のシード・永続化データでは年内運用のみを想定しており、年またぎの重複は検証されていない。
- 店舗名・代理店名の表記ゆれ(商談側
venue、卸案件側dealerとの突合キー)に対する正規化ルールは未定。 - 同一店舗・同日・同一代理店の重複シフト登録を防ぐバリデーションは、実装確認の結果無いと判明した。作成フォーム(
NewShiftSheet.submit())は日付・店舗・代理店が非空であることのみを検証し、既存シフトとの重複はチェックしない。useEvents.addShift()側にも重複排除ロジックは無い。2026-08-19定例会で「同一店舗・同日の複数催事/複数代理店はない(1店舗1催事の前提でよい)」という業務ルールは確定したが、これをシステム側で一意制約として強制するかどうかの粒度は未定。 - 終了報告の締切運用(当日中か翌朝までか)は未定。督促フローについては、2026-08-19定例会で終了報告未提出アラートの追加が確定したが(未実装)、送信タイミング・締切時刻の具体値は未定。終了報告アポ数と実登録数の乖離アラート(登録漏れ通知)は判定・可視化・通知台帳のルール定義(
event-appointment-gap)まで実装済みだが、実際のメール/LINE/Slackへの自動送信配線(会社別の手段振り分けを含む)は未着手である(前述「通知」節参照)。 - 社内担当者が代理店に代わって開始報告・終了報告を代行入力できるかは、実装確認の結果、技術的には可能(
EventShiftDetail.tsxはロールを一切参照しない)と判明した。これを許容する運用にするか、明示的に禁止するガードを追加するかは未定。 - 通知手段のうちLINE・Slack連携の詳細仕様(会社ごとの手段切り替えの実装方式、送信タイミング、認証方法等)は未確認。メール通知はシンプルな仕様で先行実装できる見込み。
- 担当ロールのすり替え運用の後続設計: 催事のアポ→訪問1回目→クロージング(訪問2回目)の工程で、担当が別会社・別部署にすり替わるケース(催事のアポだけを行う会社、商談だけを担う別会社等)をデータ上追跡できるようにする必要がある、という重要論点が2026-08-19定例会で共有された。案件ごとにあらかじめ数フェーズ(例: 5フェーズ)を用意し「どのフェーズを誰が担当するか」を登録できる状態にしたいが、登録時点では担当を割り当てられず、後から(週次でまとめて等)割り振るパターンが多い運用上の制約がある。本件は先行スコープではなく後続の設計課題として、Mockに反映しながら検討する。