MAS-129: 見積中ORDの複数見積比較機能
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-129 |
| カテゴリ | 新機能(UI・ドメイン) |
| Phase | P2 |
| 優先度 | ★(中) |
| 所要時間 | M |
| 対象ファイル | 000_infra/003_contracts.js, 100_config/101_sys_config.js, 400_domain/408_quote_comparison.js(新規), templates/quote_comparison_dialog.html(新規) |
| 前提案件 | MAS-125(発注ステータス遷移ワークフロー標準化に影響を与える) |
目的
31_wrk_order シートで同一案件に対し複数ベンダーから取得した見積(発注ステータス = 見積中 のORD)が並列に存在する場合に、それらを横並びで比較し、HTMLダイアログ上で採用ベンダーを決定する操作をシステム化する。
採用決定時には、選択ORDを 発注済 に昇格させ、同一案件の非選択ORDを 失注 に一括更新することで、相見積取得から発注決定までのプロセス(採用根拠の証跡)をシート上に残す。
これにより:
- 案件単位での相見積記録が可能になり、調達ガバナンスの統制が効く
- 採用判断を1操作で完結でき、複数行の手動更新ミス(採用と非採用の取り違え・更新漏れ)を防げる
- 「失注」ステータスの導入により、MAS-125(発注ステータス遷移標準化)の前提パスが「見積中→発注済→検収済」「見積中→失注」の2系統で確定する
現在のコード(修正対象)
000_infra/003_contracts.js — OrderDTO 型定義(L13-36)
/**
* 31_wrk_order — 発注レコード
* @typedef {Object} OrderDTO
* @property {boolean} 有効フラグ
* @property {string} 発注ID(ORD) - "ORD_YYYYMMDD_NNNN"
* @property {Date} 起票日時
* @property {string} 起票者
* @property {string} 取引先名
* @property {string} 契約・件名
* @property {string} 摘要
* @property {string} 契約形態 - "スポット" | "継続"
* @property {string} 開始年月 - "YYYY-MM"
* @property {string} 終了年月 - "YYYY-MM"
* @property {number} 税抜金額_発注
* @property {number} 消費税額_発注
* @property {number} 税込金額_発注
* @property {number} 発注残高(自動計算)
* @property {string} PJ名
* @property {string} 組織名
* @property {string} 発注ステータス - "見積中" | "発注済" | "検収済"
* @property {string} 参照元区分 - "SUB" | "HC" | "CAPEX" 等
* @property {string} 参照元ID
* @property {string} 証憑URL
*/
100_config/101_sys_config.js — WRK_ORDR ヘッダー定義(L846)
'WRK_ORDR': { headers: ["有効フラグ","発注ID(ORD)","起票日時","起票者","取引先名","契約・件名","摘要","契約形態","開始年月","終了年月","税抜金額_発注","消費税額_発注","税込金額_発注","発注残高(自動計算)","PJ名","組織名","発注ステータス","参照元区分","参照元ID","証憑URL"], color: "#b45f06" },
100_config/101_sys_config.js — MST_DICT 自動挿入の 発注ステータス 定義(L1301)
['発注ステータス', [['ORD_EST','見積中'], ['ORD_ORD','発注済'], ['ORD_PAR','部分納品'], ['ORD_CMP','完了'], ['ORD_CAN','取消']]],
100_config/101_sys_config.js — WRK_ORDR のバリデーション設定(L1416-1419)
setVali('WRK_ORDR', 5, 'E', '31_wrk_order'); // 取引先名 (E列)
setVali('WRK_ORDR', 15, 'H', '31_wrk_order'); // PJ名 (O列)
setVali('WRK_ORDR', 16, 'G', '31_wrk_order'); // 組織名 (P列)
setVali('WRK_ORDR', 17, 'S', '31_wrk_order'); // 発注ステータス (Q列)
000_infra/002_constants.js — MENU_DEFINITION (L206-)
メニューは onOpen() の中で直接 ui.createMenu() するのではなく、Constants.MENU_DEFINITION 配列の宣言定義を onOpen() がループ生成する。新メニュー項目はこの配列に追加する。サイドバー側にも自動的に反映される(source: 'sidebar' 指定の場合)。
200_data/202_repository.js — OrderRepository(L107-146)
findAll() は { headers, dtos } を返し、save(dtos) は内部の writeDtosToSheet_() 経由でシートを全置換書き込みする。採用決定処理では「findAll() で全件取得 → 該当DTOを更新 → save(全dtos) で全置換」のパターンとする(部分更新APIは存在しない)。
修正方針
機能を以下の3フェーズに分けて実装する:
フェーズ① 見積紐付け(案件IDの採番と一括設定)
契機: ユーザーが 31_wrk_order シートで「見積中」ステータスのORD行を1行以上選択し、メニュー 📋 サイドバー: 📦 発注管理 → 🔗 見積を案件IDで紐付け をクリック。
実装: 400_domain/408_quote_comparison.js の assignCaseId_()
SpreadsheetApp.getActiveSheet().getName()が31_wrk_orderでなければUtils.toastResult('assignCaseId_', '31_wrk_orderシートで実行してください', 5)で中断SpreadsheetApp.getActiveSheet().getActiveRangeList()で選択範囲を取得し、行番号集合を構築- ヘッダー行を取得し、
headers.indexOf('発注ステータス')/headers.indexOf('案件ID')/headers.indexOf('発注ID(ORD)')で列インデックスを取得 - 選択行のステータスを検査:
- 選択行0件 → エラー通知して中断
- 「見積中」以外のステータスが含まれる → エラー通知して中断
- 既に「案件ID」が設定済みの行が含まれる →
SpreadsheetApp.getUi().alert()で上書き確認、キャンセルなら中断
generateCaseId_()でCASE_YYYYMMDD_NNNN形式のIDを生成- 選択行の「案件ID」列セルに
setValueで一括書き込み(ヘッダーインデックスベース) Utils.toastResult('assignCaseId_', '案件ID ' + caseId + ' を ' + 行数 + ' 件に紐付けました', 5)で完了通知
generateCaseId_() ヘルパー: 400_domain/408_quote_comparison.js 内に独立関数として実装する(Constants.ID_PREFIX_MAP には追加しない。ID_PREFIX_MAP の各エントリは特定シートの pattern と紐付くフォーマットであり、シート横断の論理キーである案件IDの設計意図と合わない)。
function generateCaseId_() {
var today = new Date();
var ymd = Utilities.formatDate(today, 'JST', 'yyyyMMdd');
// 同日内連番: 31_wrk_orderの全「案件ID」列をスキャンし、CASE_YYYYMMDD_ 接頭辞の最大連番+1
var sheet = OrderRepository._getSheet // ※ private 呼び出し回避のため findAll() 経由が望ましい
var found = OrderRepository.findAll();
var caseIdx = found.headers.indexOf('案件ID');
var maxSeq = 0;
if (caseIdx >= 0) {
var prefix = 'CASE_' + ymd + '_';
found.dtos.forEach(function(dto) {
var v = String(dto['案件ID'] || '');
if (v.indexOf(prefix) === 0) {
var n = parseInt(v.slice(prefix.length), 10);
if (!isNaN(n) && n > maxSeq) maxSeq = n;
}
});
}
return 'CASE_' + ymd + '_' + ('0000' + (maxSeq + 1)).slice(-4);
}
フェーズ② 比較ビュー表示(HTMLダイアログ)
契機: ユーザーが 31_wrk_order シートで案件ID紐付け済みのORD行を1行選択し、メニュー 📋 サイドバー: 📦 発注管理 → 🔍 見積比較ビューを開く をクリック。
実装: 400_domain/408_quote_comparison.js の showQuoteComparison_()
- 選択行が0件 / 2件以上 → エラー通知して中断
- ヘッダーから「案件ID」列インデックスを取得し、選択行から
caseIdを取得 caseIdが空文字 → 「案件IDが未設定です。先に「🔗 見積を案件IDで紐付け」を実行してください。」とエラー通知して中断OrderRepository.findAll()を実行し、同一案件IDを持つDTOを抽出HtmlService.createTemplateFromFile('templates/quote_comparison_dialog')でテンプレート読込- テンプレート変数として
caseId,quotes(DTOの配列),isSingleQuote(1件のみフラグ)を渡す template.evaluate().setWidth(900).setHeight(600)でモーダル設定SpreadsheetApp.getUi().showModalDialog(html, '見積比較: ' + caseId)で表示
templates/quote_comparison_dialog.html (新規):
- 上部: 案件ID表示、件数、注記(1件のみの場合は「比較対象が1件のみです」)
- 中央: HTMLテーブル(列: 取引先名 / 契約形態 / 開始年月 / 終了年月 / 税込金額_発注 / 摘要 / 発注ステータス / 採用ボタン)
- 各行に
<button onclick="adopt('ORD_xxx', '取引先名表示用')">この見積を採用</button>を配置 - 採用ボタンの可用性:
発注ステータス === '見積中'の行のみ有効化(disabled属性で他は非活性化) - クライアントサイドJS:
function adopt(ordId, partnerName) {
if (!window.confirm('「' + partnerName + '」の見積を採用します。同一案件の他の見積はすべて「失注」になります。よろしいですか?')) return;
document.getElementById('busy').style.display = 'block';
google.script.run
.withSuccessHandler(onAdoptSuccess)
.withFailureHandler(onAdoptError)
.adoptQuote_(ordId, '<?= caseId ?>');
}
function onAdoptSuccess(result) {
document.getElementById('busy').style.display = 'none';
if (result && result.success) {
alert('採用が完了しました。');
google.script.host.close();
} else {
alert('エラー: ' + (result && result.message ? result.message : '不明なエラー'));
}
}
function onAdoptError(err) {
document.getElementById('busy').style.display = 'none';
alert('通信エラー: ' + (err && err.message ? err.message : err));
}
フェーズ③ 採用決定とステータス一括更新
実装: 400_domain/408_quote_comparison.js の adoptQuote_(ordId, caseId) (クライアントから呼ばれるサーバー関数)
OrderRepository.findAll()で{ headers, dtos }を取得- 楽観的ロック検証:
dtosから案件ID === caseIdの行を抽出。全行の発注ステータスが「見積中」であることを確認。違反があれば{ success: false, message: '他の操作により発注ステータスが変更されています。画面を再読込してください。' }を返却 - 採用DTO(
発注ID(ORD) === ordIdかつ案件ID === caseId)が存在することを確認。なければ{ success: false, message: '採用対象のORDが見つかりません' } dtosをループし、案件ID === caseIdの各DTOに対して:発注ID(ORD) === ordIdならdto['発注ステータス'] = '発注済'- それ以外なら
dto['発注ステータス'] = '失注'
OrderRepository.save(dtos)で全置換書き込みUtils.logInfo('adoptQuote_', '案件 ' + caseId + ' で ORD ' + ordId + ' を採用、他' + 非採用件数 + '件を失注に更新')でログ{ success: true }を返却。クライアント側でダイアログを閉じる
例外発生時は Utils.logError('adoptQuote_', e, 'caseId=' + caseId + ' ordId=' + ordId) でスタックトレース記録し、{ success: false, message: '保存処理でエラーが発生しました: ' + e.message } を返却。
DDL・DTO・メニューの変更
| 対象 | 変更 |
|---|---|
000_infra/003_contracts.js OrderDTO | @property {string} 案件ID を末尾(証憑URL の後)に追加 |
100_config/101_sys_config.js WRK_ORDR.headers | 配列末尾に "案件ID" を追加 |
100_config/101_sys_config.js MST_DICT 自動挿入 発注ステータス | 配列末尾に ['ORD_LST','失注'] を追加(既存定義は ['ORD_EST','見積中'], ['ORD_ORD','発注済'], ['ORD_PAR','部分納品'], ['ORD_CMP','完了'], ['ORD_CAN','取消']。冪等性は existingEntries Setチェックで担保済み) |
000_infra/002_constants.js MENU_DEFINITION | 新カテゴリ 📋 サイドバー: 📦 発注管理 を追加し、{ label: '🔗 見積を案件IDで紐付け', funcName: 'assignCaseId_', description: '...' } と { label: '🔍 見積比較ビューを開く', funcName: 'showQuoteComparison_', description: '...' } を含める。source: 'sidebar' を付与 |
影響範囲
| 変更対象 | 変更内容 | 変更量 |
|---|---|---|
000_infra/003_contracts.js | OrderDTO に 案件ID プロパティを末尾追加(1行) | 1行 |
000_infra/002_constants.js | MENU_DEFINITION に新カテゴリを追加(〜10行) | ~10行 |
100_config/101_sys_config.js | WRK_ORDR ヘッダー1要素追加 + MST_DICT に「失注」1要素追加 | 2行 |
400_domain/408_quote_comparison.js(新規) | assignCaseId_() / showQuoteComparison_() / adoptQuote_() / generateCaseId_() | ~150行 |
templates/quote_comparison_dialog.html(新規) | 比較ビューHTML + クライアントJS | ~100行 |
- 既存動作への影響: 既存ORDレコードは
案件ID列が空のまま正常動作する(Contracts.toRowはヘッダー名ベースで欠損キーは空文字埋め)。発注ステータスの「失注」追加は MST_DICT の追記のみで、既存「見積中」「発注済」レコードに影響なし。 - 下流への波及:
OrderRepository.findAll()で取得する DTO に案件IDキーが追加されるが、既存の参照側コード(RPA起票・サブ元帳エンジン等)は未参照のキーを無視するため影響なしsetupAllSchemas実行でWRK_ORDR.headersの末尾に「案件ID」列が追加される(既存データの列ずれは発生しない=末尾追加のため)
注意事項
OrderRepository.save(dtos)は全置換方式:writeDtosToSheet_経由でデータ行全体をクリア → 再書き込みする。採用決定時は必ずfindAll()で全件取得 → 該当DTOを更新 →save(全dtos)を渡すこと。getRange().setValues()等のシート直接操作は禁止(ヘッダー以外の行・バリデーションが破壊されるリスク)。- DDL列追加は末尾追加のみ:
WRK_ORDR.headersへの「案件ID」追加は末尾追加とすること。中間挿入はContracts.toDto/Contracts.toRowのヘッダーインデックス対応により、既存データの列ずれが発生する可能性があり禁止。 google.script.runには必ずwithFailureHandlerを設定: ネットワークエラーや例外がサイレントに失敗するのを防ぐため、withSuccessHandlerとwithFailureHandlerを必ずペアで設定する。- 採用確認はクライアントサイドの
window.confirm()で実装: HTMLダイアログ表示中にサーバーサイドからSpreadsheetApp.getUi().alert()を呼ぶと、モーダル同士の干渉により予期しない挙動(ダイアログがフリーズ・閉じない等)を引き起こす可能性がある。確認はクライアントJS内で完結させる。 Utils.toastResult(funcName, message, duration)の第1引数は呼び出し元の関数名(文字列): 例:Utils.toastResult('assignCaseId_', 'エラー', 5)。デバッグログ・監査ログとの突合に必要。- 列参照は必ず
headers.indexOf('列名')で取得: 列番号ハードコード禁止(CLAUDE.md コーディング規約)。 generateCaseId_()の同時実行: GAS の同時実行制限(最大30並列)下では、複数ユーザーが同時に紐付け実行すると同一連番が採番される競合リスクがある。本案件ではLockServiceまでは導入せず(実運用上は経理担当1名想定)、注意事項として明記するに留める。将来的に必要になればLockService.getScriptLock()でラップする方針。- 「失注」追加とCLAUDE.md/DTO型注釈の差異: 既存の
OrderDTOJSDoc は"見積中" | "発注済" | "検収済"だが、DDL実体(MST_DICT 自動挿入)では見積中/発注済/部分納品/完了/取消が定義されている。本案件では DDL に「失注」を追加すると同時に、JSDoc の@property {string} 発注ステータス行末コメントも"見積中" | "発注済" | "失注" | ...形式に更新して整合性を取る(現行の不整合を完全には解消しないが、新規追加分は反映)。
エッジケース
| 条件 | 表示値・動作 | 理由 |
|---|---|---|
見積紐付け実行時にアクティブシートが 31_wrk_order 以外 | Utils.toastResult('assignCaseId_', '31_wrk_orderシートで実行してください', 5) で中断 | 誤シート操作防止 |
| 見積紐付け時に選択行が0件 | Utils.toastResult('assignCaseId_', '紐付け対象の行を選択してください', 5) で中断 | ガード処理 |
| 選択行に「見積中」以外のステータス混在 | Utils.toastResult('assignCaseId_', '選択行に見積中以外のステータスが含まれています', 5) で中断 | 既に発注済/失注の行への誤った再紐付けを防止 |
| 選択行の「案件ID」が既に設定済み(一部または全部) | SpreadsheetApp.getUi().alert(..., ButtonSet.OK_CANCEL) で上書き確認、OKなら新IDで上書き、キャンセルは中断 | 誤った案件統合の防止と、明示的なユーザー判断の確保 |
| 比較ビュー実行時に選択行が0件 | Utils.toastResult('showQuoteComparison_', '比較対象の行を1件選択してください', 5) で中断 | ガード処理 |
| 比較ビュー実行時に選択行が2件以上 | Utils.toastResult('showQuoteComparison_', '1行のみ選択してください', 5) で中断 | 案件IDの一意特定のため |
| 比較ビュー実行時に対象行の「案件ID」が空 | Utils.toastResult('showQuoteComparison_', '案件IDが未設定です。先に「🔗 見積を案件IDで紐付け」を実行してください', 5) で中断 | 操作順序の誘導 |
| 同一案件IDのORDが1件のみ | ダイアログを表示するが、ヘッダー部に「⚠️ 比較対象が1件のみです」と注記表示。採用ボタンは有効化(単独でも採用決定可能) | 情報提供+単独見積でも発注確定の意思表示記録に使えるようにするため |
| 採用決定時に対象案件IDのいずれかのORDが「見積中」以外(楽観的ロック失敗) | adoptQuote_ が { success: false, message: '他の操作により発注ステータスが変更されています。画面を再読込してください。' } を返却。クライアントで alert() 表示し、ダイアログは閉じずに再操作可能に | 二重採用・並行操作による上書き事故防止 |
採用決定時に ordId が同一案件IDのDTOに見つからない | { success: false, message: '採用対象のORDが見つかりません' } を返却 | クライアント・サーバー間の不整合検知 |
OrderRepository.save() 実行中に例外発生 | Utils.logError('adoptQuote_', e, 'caseId=...') でスタックトレースを監査ログに記録、{ success: false, message: '保存処理でエラーが発生しました: ' + e.message } を返却 | エラー追跡可能性の担保 |
| HTMLダイアログ表示中にスプレッドシート側で対象行の「案件ID」を手動変更 | 採用ボタンクリック時の楽観的ロック検証で caseId 一致行が0件になり、{ success: false, message: '案件IDに該当するORDが見つかりません' } を返却 | 並行編集による不整合検知 |
案件ID 列追加前のシートで紐付けメニューを実行 | headers.indexOf('案件ID') === -1 を検出し、Utils.toastResult('assignCaseId_', '案件ID列が未追加です。setupAllSchemasを実行してください', 5) で中断 | DDL適用漏れの誘導 |
実データ検証
実装前および動作確認時に以下の項目を確認すること:
| 確認項目 | 確認方法 | 理由 |
|---|---|---|
既存 31_wrk_order の 発注ステータス 実データ値 | シートのQ列を確認し、現在使われている値(見積中/発注済/検収済 等)の表記を抽出 | 「見積中」の完全一致比較が成立することを検証(DDLコード値 vs 実データの乖離検出。失敗パターン #3 対策) |
setupAllSchemas 実行後の 31_wrk_order 末尾列状態 | DDL更新後にシートを開き、U列(21列目)として「案件ID」列が追加され、A〜T列の既存データが列ずれしていないことを目視確認 | 列追加の安全性確認 |
MST_DICT への「失注」追加の冪等性 | setupAllSchemas を2回連続実行しても MST_DICT の 発注ステータス::失注 行が1件のみで重複していないことを確認 | 既存の existingEntries Setチェックの動作確認 |
dropSheet の S列(UI発注ステータス)への「失注」反映 | setupAllSchemas 実行後に MST_DROP シートのS列を確認し、FILTER 数式により「失注」が選択肢として表示されることを確認 | バリデーションプルダウンへの反映確認 |
既存 OrderRepository.findAll() 呼び出し側への影響 | 400_domain/410_subledger_engine.js 等の参照箇所をGrepし、新規追加した 案件ID キーが既存ロジックに干渉しないことを確認 | 後方互換性の確認 |
関連ドキュメント
| 仕様書 | 関連箇所 |
|---|---|
| CLAUDE.md | 「データアクセス」「コーディング規約」「Human-in-the-Loop」「ファイル番号体系」 |
| MAS-125(発注ステータス遷移ワークフロー標準化) | 本案件で「失注」ステータスを追加することにより、発注ステータス の遷移パスが「見積中→発注済→検収済」「見積中→失注」の2系統で確定する。MAS-125のワークフロー設計はこの遷移パスを前提に進める |
| MAS-131(失注レコードアーカイブ) | 本案件はステータスを「失注」に変更するのみ。レコードの物理削除・アーカイブはMAS-131のスコープ |
| MAS-133(関連削除ワークフロー) | 失注したORDに紐付くINV/STLが既に存在する場合の関連削除ロジックはMAS-133スコープ |
| 202_repository.js | OrderRepository.findAll() / save() の挙動(save は writeDtosToSheet_ 経由の全置換) |
| 003_contracts.js | OrderDTO 型定義および Contracts.toDto / Contracts.toRow のヘッダー名ベース変換 |
人間が検討すべき事項
docs/_internal/TODO_future.md の MAS-129 行に記載された検討事項:
- 案件IDの採番ルール(手動 or 自動): 本仕様では「自動採番(
CASE_YYYYMMDD_NNNN)」を採用。手動採番を許容するとIDフォーマットの揺れ・重複リスクがあるため。手動指定が必要な場合は将来仕様としてassignCaseIdManual_()を追加検討 - 失注見積の保持期間: 本案件スコープではステータス変更のみ実施。物理削除・アーカイブはMAS-131・MAS-133で対応
- MAS-125(ステータス標準化)と合わせた設計: 本案件で
発注ステータスの遷移パスが「見積中→発注済→検収済」「見積中→失注」の2系統で確定。MAS-125実装時には、これら遷移パスのみを許容する onEdit ガード or バリデーションの実装を検討すること
本案件の実装方針として追加で人間判断が必要な事項:
- 比較ビューでの表示項目の取捨選択: 初版では「取引先名 / 契約形態 / 開始年月 / 終了年月 / 税込金額_発注 / 摘要 / 発注ステータス」の7項目を表示。納期・条件・支払サイト等の追加要件があれば次版で拡張
- 採用後の自動INV起票連動: 本案件は
発注ステータス更新のみ。採用ORDから INV/STL を自動起票するワークフローは別案件(パイプライン RPA・SubledgerService 連動)で検討 - 「失注」ステータスのフィルタリング規約: 既存マート集計(602/603/604/605)が
発注残高等で失注ORDを集計対象にしないか確認が必要。失注ORDは税込金額_発注計上対象から除外すべきだが、該当ロジックがない場合は別案件で対応 - HTMLダイアログ vs サイドバー方式の選択理由: モーダルダイアログ(
showModalDialog)を採用。サイドバー(showSidebar)はoperations_sidebar.htmlで全操作を集約する用途で既に使用中。比較ビューは「単発操作」性質が強くダイアログが適切。ユーザー操作完了後は自動クローズが期待されるため
実装プロンプト(Claude Code 用)
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-129「見積中ORDの複数見積比較機能」を実装してください。
## 実行前タスク
以下のファイルを Read で確認してから実装に入ってください:
1. `000_infra/003_contracts.js` — `OrderDTO` 型定義(L13-36)。全プロパティ並び順と `発注ステータス` の選択肢
2. `000_infra/002_constants.js` — `MENU_DEFINITION` 配列(L206-)。実在するカテゴリ名(`📋 サイドバー: ...` 形式)と `source: 'sidebar'` の使い方
3. `200_data/202_repository.js` — `OrderRepository.findAll()` / `save()` / `_getSheet()` の実装(L107-146)。`save()` が `writeDtosToSheet_` 経由の全置換方式であることを確認
4. `100_config/101_sys_config.js` — `WRK_ORDR.headers` 定義(L846)、`MST_DICT` 自動挿入の `発注ステータス` カテゴリ(L1301)、`setVali('WRK_ORDR', ...)`(L1416-1419)
5. `000_infra/004_utils.js` — `Utils.toastResult` (L419) / `Utils.logInfo` (L393) / `Utils.logError` (L403) のシグネチャ
6. `templates/operations_sidebar.html` — 既存の `HtmlService` テンプレート構造(参考)
7. `CLAUDE.md` — コーディング規約(データアクセス・列参照・Human-in-the-Loop)
8. `docs/dev/dev_mas-129_quote_comparison.md` — 本仕様書
## 修正対象ファイル
- `000_infra/003_contracts.js`: `OrderDTO` JSDoc に `@property {string} 案件ID` を末尾追加
- `000_infra/002_constants.js`: `MENU_DEFINITION` 配列に新カテゴリ `📋 サイドバー: 📦 発注管理` を追加(`source: 'sidebar'` 付与、`assignCaseId_` と `showQuoteComparison_` の2項目)
- `100_config/101_sys_config.js`: `WRK_ORDR.headers` の末尾に `"案件ID"` を追加 + `MST_DICT` 自動挿入の `発注ステータス` 配列末尾に `['ORD_LST','失注']` を追加
- `400_domain/408_quote_comparison.js`(新規): `assignCaseId_()` / `showQuoteComparison_()` / `adoptQuote_()` / `generateCaseId_()` の実装
- `templates/quote_comparison_dialog.html`(新規): 比較ビューHTML + クライアントJS(`google.script.run` 呼び出し含む)
## 実装内容
仕様書 `docs/dev/dev_mas-129_quote_comparison.md` の「修正方針」セクションに記載した3フェーズ実装手順に従うこと。
### A. DDL・DTO・メニュー変更
- `OrderDTO` の JSDoc に `案件ID` プロパティを末尾追加
- `WRK_ORDR.headers` 配列の末尾に `"案件ID"` を追加(中間挿入禁止)
- `MST_DICT` 自動挿入の `発注ステータス` カテゴリ配列末尾に `['ORD_LST','失注']` を追加(既存の `existingEntries` Setチェックで冪等性が担保される)
- `MENU_DEFINITION` に `{ category: '📋 サイドバー: 📦 発注管理', source: 'sidebar', items: [...] }` を追加
### B. `400_domain/408_quote_comparison.js` の実装
- `assignCaseId_()`: アクティブシート判定 → 選択行取得 → ステータス検査 → 案件ID生成 → 一括書き込み → トースト通知
- `showQuoteComparison_()`: 選択行検証 → 案件ID取得 → 同一案件のORD抽出 → HTMLテンプレート評価 → モーダルダイアログ表示
- `adoptQuote_(ordId, caseId)`: `findAll()` → 楽観的ロック検証 → 採用/失注の振り分け → `save()` → `{ success: true/false, message }` 返却
- `generateCaseId_()`: `OrderRepository.findAll()` から `CASE_YYYYMMDD_` 接頭辞の最大連番+1 で `CASE_YYYYMMDD_NNNN` を生成
### C. `templates/quote_comparison_dialog.html` の実装
- HTMLテーブルで見積一覧を表示
- 各行に「この見積を採用」ボタン、`発注ステータス !== '見積中'` の行は `disabled`
- `window.confirm()` で採用確認 → `google.script.run.withSuccessHandler(...).withFailureHandler(...).adoptQuote_(ordId, caseId)` を呼ぶ
- 成功時: `alert('採用が完了しました')` → `google.script.host.close()`
- 失敗時: `alert('エラー: ' + result.message)` でメッセージ表示し、ダイアログは閉じない(再操作可)
## 制約
- `OrderRepository.save(dtos)` 以外でのシート直接書き込み(`getRange().setValues()` 等)は禁止
- 列番号ハードコード禁止。`headers.indexOf('列名')` でインデックス取得すること
- `setupAllSchemas` の変更は冪等性を担保すること(既存の `existingEntries` Setチェックで自動的に担保される構造を踏襲)
- メニュー追加は `Constants.MENU_DEFINITION` への追加のみ。`onOpen()` 関数内の `ui.createMenu()` を直接編集しないこと(動的生成のため不要)
- `Utils.toastResult(funcName, message, duration)` の第1引数は呼び出し元の関数名(文字列)
- `google.script.run` 呼び出しには `withFailureHandler` を必ず設定すること
- 採用確認はクライアントサイドの `window.confirm()` で実装(サーバーサイドからの `SpreadsheetApp.getUi().alert()` はHTMLダイアログ表示中に干渉リスクあり)
- `WRK_ORDR.headers` への列追加は末尾追加のみ。中間挿入は禁止(列ずれによるデータ破損防止)
## エッジケース
仕様書「エッジケース」セクションに記載した全条件を実装すること。特に以下は必須:
- 楽観的ロック(採用決定時の `案件ID` 一致行の `発注ステータス === '見積中'` 再検証)
- `withFailureHandler` によるHTMLダイアログ内エラー表示
- 「案件ID」列が未追加の場合のガード(`headers.indexOf('案件ID') === -1` 検出)
- 「見積中」以外のステータス混在時の中断
- 既存「案件ID」設定済み行の上書き確認
## 動作確認
1. `npm run push:dev` で開発環境にデプロイ
2. GASエディタから `setupAllSchemas` を実行し、`31_wrk_order` のU列(21列目)に「案件ID」列が追加され、`MST_DICT` の S列(UI発注ステータス)に「失注」が表示されることを確認
3. `31_wrk_order` に「見積中」ステータスのORD行を取引先違いで2件以上手動追加(または既存行のステータスを「見積中」に変更)し、複数選択してメニュー `📋 サイドバー: 📦 発注管理 → 🔗 見積を案件IDで紐付け` を実行 → 同一 `CASE_YYYYMMDD_NNNN` が全選択行のU列に書き込まれることを確認
4. 紐付け済みのORD行を1行選択し、メニュー `🔍 見積比較ビューを開く` を実行 → モーダルダイアログがHTMLテーブル形式で表示されることを確認
5. ダイアログ内の「この見積を採用」ボタンをクリック → `window.confirm` ダイアログが表示されることを確認
6. 確認OKをクリック → 採用ORDが「発注済」、同一案件IDの他ORDが「失注」に更新されること、ダイアログが自動クローズすることをシートで確認
7. **楽観的ロックのテスト**: 比較ダイアログを開いたまま別タブで対象案件IDのいずれかのORDの `発注ステータス` を手動変更(例: 「失注」に)し、ダイアログ側で別ORDの採用ボタンをクリック → サーバーから `success: false` が返り、エラーメッセージが `alert` 表示され、ダイアログは閉じないことを確認
8. **エッジケースの確認**:
- `31_wrk_order` 以外のシートで紐付けメニューを実行 → 「31_wrk_orderシートで実行してください」と表示
- 「発注済」ステータスを含む行を選択して紐付け → ステータス混在エラー
- 案件ID未設定の行を選択して比較ビュー → 「案件IDが未設定です」エラー
- 単独見積(同一案件IDが1件のみ)の状態で比較ビュー → 「比較対象が1件のみです」注記が表示されるが採用は可能
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| Phase 1(ファイル調査・設計確定) | あり | ファイル名・関数名・行番号・列位置・メニュー構造を全確定 |
| Phase 2(仕様書清書) | 最小限 | Phase 1確定内容の書き下しのみ |
| DDL変更(003_contracts・002_constants・101_sys_config) | あり | 既存パターンへの末尾追記。冪等性担保構造の理解が必要 |
| 400_domain/408_quote_comparison.js 機能実装 | あり | 楽観的ロック・全置換書き込みパターンの正確な適用 |
| HTMLダイアログ実装 | 最小限 | テンプレート的な HTML/JS。`google.script.run` 連携パターンの踏襲 |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| 仕様書作成(本ドキュメント) | Claude Opus 4.7 | 既存DTO・Repository・MENU_DEFINITION・DDL構造の横断分析、CASE_採番方式の選定、楽観的ロック設計、HTMLダイアログ干渉リスクの評価に高い推論力が必要 |
| DDL変更(003_contracts・002_constants・101_sys_config) | Sonnet | 既存パターンへの末尾追記。挿入位置の判断と冪等性確認が必要 |
| 機能実装(408_quote_comparison.js) | Sonnet | 複数ファイル横断(Repository / Utils / HtmlService)だが仕様書で設計確定済み。楽観的ロック実装の判断ポイントあり |
| HTMLダイアログ実装 | Haiku | テンプレート的な HTML/JS。google.script.run 連携パターンが既存にあれば踏襲 |
| 動作確認 | ユーザー手動 | GASエディタでのメニュー操作・モーダル操作・並行編集テストが必要 |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-22 | 初版作成。31_wrk_orderへの案件ID(CASE_)紐付け・HTMLダイアログ比較ビュー・採用決定(発注済/失注一括更新)の3フェーズ設計。MAS-125発注ステータス遷移ワークフローへの影響を明記 |
仕様書作成プロンプト(再現性・監査性のため必ず記録)
展開して表示
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**: Phase 1(設計)ではフル活用し、ファイル名・関数名・行番号・列位置・エッジケース・Step分割粒度を完全確定させる。Phase 2(清書)では各Step内で最小限に抑え、Phase 1確定済み内容の書き下しに徹する。出力途中で「本当にこれで良かったか」と再考しない。
2. **テキスト報告の禁止**: 「〜を作成します」等のtextのみでtool_useなしにturnを終了しない。説明は1文以内。直ちにtoolを呼ぶ。
3. **4-5分割のWrite/Edit実行**: 2-1(骨格~20行) / 2-2(概要〜注意事項~300行) / 2-3a(エッジケース〜人間検討事項~200行) / 2-3b(実装プロンプト〜変更履歴~250行) / 2-4(`<details>`プロンプト全文記録・最重量・必ず独立Step)に分割。1回のWrite/Editは約300行以内。
4. **各Stepで何を書くかを具体指示**: 設計判断をPhase 2実行時に持ち込まない。各Stepの内容はこのプロンプトに箇条書きで明記済み。
======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 MAS-129「見積中ORDの複数見積比較機能」の開発仕様書を作成してください。
完成後は `docs/_config.json` の `nav` 配列の適切なセクションにも必ず追記してください。
---
## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)
Phase 2の清書時に設計判断を再考しなくて済むよう、以下を全て実行してから仕様書作成に着手すること。
### 1-A: 案件定義の読み込み
- `docs/_internal/TODO_future.md` で MAS-129 の行を検索し、案件名・概要・人間が検討すべき事項を取得する。
### 1-B: プロジェクト規約・仕様書テンプレートの読み込み
- `CLAUDE.md` を読み込み、コーディング規約(データアクセス・会計ロジック・Human-in-the-Loop)・ファイル番号体系・ブランチ運用を把握する。
- `docs/_internal/dev_spec_prompt_template.md` の「Phase 2: 仕様書の作成」→「セクション構成(必須)」と「実装プロンプトのフォーマット」を読み込み、必須セクション構成を把握する。
- `docs/dev/dev_mas-094_boundary_month_selector.md`(UIメニュー追加系の参考)を Read し、フォーマットを把握する。
### 1-C: 関連GASコードの調査
**Grep は「どこにあるか」の発見まで。型・フィールド名・シート名・メニュー名・定数名は必ず Read で裏取りすること。推測した瞬間に手を止めて Read する。**
1. **`000_infra/003_contracts.js`**
- `OrderDTO` の全プロパティを Read で確認する。特に `発注ステータス` の現在の選択肢(`"見積中" | "発注済" | "検収済"`)と、各プロパティの並び順(新規 `案件ID` プロパティの挿入位置を決定するため)を確認する。
- `Contracts.toDto` / `Contracts.toRow` の変換ロジックがヘッダー名ベースであることを確認する。
2. **`000_infra/002_constants.js`**
- `ID_PREFIX_MAP` の各エントリの型 `{ pattern, prefix, digit, isDate }` を Read で確認する。`CASE_` プレフィックスをここに追加するか判断する(現状の各エントリは特定シートの `pattern` との対応付けが前提。シートに紐付かない横断IDは `ID_PREFIX_MAP` の設計意図と合わない可能性があるため、独立した採番ヘルパー関数として実装する案と比較検討すること)。
- `SHEET_DEFAULTS` の `31_wrk_order` エントリ(`{ pattern: '31_wrk_order', defaults: { '発注ステータス': '見積中', '契約形態': 'スポット' } }`)を確認し、デフォルト値への影響範囲を把握する。
3. **`200_data/202_repository.js`**
- `OrderRepository._getSheet()`(`Utils.getSheetByKey('WRK_ORDR', '31_wrk_order')` を使用)・`findAll()`・`save(dtos)`・`append(dtos)` を Read で確認する。
- `save()` が `writeDtosToSheet_`(全置換方式)であることを把握し、採用決定時に `findAll()` → 全件更新 → `save()` の流れが必要なことを確認する。
4. **`100_config/101_sys_config.js`**
- `setupAllSchemas` 関数内で `31_wrk_order`(または `WRK_ORDR` キー)のDDLを定義している箇所を Read で確認し、現在の列定義・データ入力規則(プルダウン)の記述形式と冪等性担保の方法(既存列存在チェックのパターン)を把握する。
- `onOpen()` 内の `ui.createMenu` を Read し、**実在するメニュー名・サブメニュー名を文字列で確認する**(造語禁止)。新機能のメニュー項目追加先を決定する。
5. **`300_ui/301_ui_assist.js`(および `300_ui/` 配下の全ファイル)**
- `HtmlService.createHtmlOutputFromFile` を使ったモーダルダイアログの既存実装パターンを調査する。存在する場合はそのHTMLファイル名・関数名・`google.script.run` の使い方を確認する。
- 存在しない場合は「新規実装」として記録し、HTMLファイル名(例: `quote_comparison_dialog.html`)と配置先(`300_ui/` 推奨)を決定する。
6. **`400_domain/` 配下のファイル一覧をGrepし**、新機能の実装ファイルとして適切な番号(`408_` 以降で未使用のもの)を確定する。または `300_ui/301_ui_assist.js` への追記が適切かを判断する。
### 1-D: 設計決定事項の確定チェックリスト(Phase 2開始前に全項目を埋めること)
- [ ] 新規実装ファイルのパス(例: `400_domain/408_quote_comparison.js` または `300_ui/301_ui_assist.js` への追記)
- [ ] HTMLダイアログファイルのパス(例: `300_ui/quote_comparison_dialog.html`)
- [ ] `CASE_` 採番方法(`ID_PREFIX_MAP` 追加 or 独立ヘルパー関数 `generateCaseId_()` として実装)
- [ ] `OrderDTO` への `案件ID` プロパティの追加位置(**末尾追加を推奨。既存プロパティの中間挿入はシートの列ずれを引き起こすリスクがある**)
- [ ] `31_wrk_order` DDL列追加の位置(**末尾追加必須。B列等の中間挿入は既存データの列ずれを引き起こすため禁止**)
- [ ] `発注ステータス` プルダウンへの「失注」追加の DDL 記述形式(`101_sys_config.js` の実際のコードに倣う)
- [ ] メニュー追加先(`onOpen()` を Read して確認した実在するメニュー名・サブメニュー名)
- [ ] 採用確認ダイアログの実装場所(HTMLダイアログ内のクライアントサイドJS `window.confirm()` 推奨。`google.script.run` からの `SpreadsheetApp.getUi().alert()` はHTMLダイアログ表示中に干渉するリスクがあるため要注意)
---
## Phase 2: 仕様書の分割作成
**出力先: `docs/dev/dev_mas-129_quote_comparison.md`**
**【絶対に1回のツール呼び出しで全内容を出力しない。以下の5 Stepに分割して実行する。】**
### Step 2-1: 骨格の作成 (Write, ~20行)
以下の見出しのみ含む骨格ファイルを Write で作成する。本文は空で可。
# MAS-129: 見積中ORDの複数見積比較機能
## 概要
## 目的
## 現在のコード(修正対象)
## 修正方針
## 影響範囲
## 注意事項
## エッジケース
## 実データ検証
## 関連ドキュメント
## 人間が検討すべき事項
## 実装プロンプト(Claude Code 用)
## 推奨実行モデル
## 変更履歴
## 仕様書作成プロンプト(再現性・監査性のため必ず記録)
### Step 2-2: 概要〜注意事項の追記 (Edit or Bash heredoc, ~300行)
以下の内容を追記する。Phase 1で確定したファイルパス・行番号・メニュー名をリテラルで埋め込むこと(プレースホルダーは残さない)。
**概要テーブル**: 案件ID=MAS-129、カテゴリ=新機能(UI・ドメイン)、Phase=2、優先度=中、所要時間=M、対象ファイル=Phase 1確定値、前提案件=MAS-125(ステータス遷移設計に影響)
**目的**: `31_wrk_order` シートで同一案件に複数ベンダーの見積ORDが存在する場合に、横並び比較とHTMLダイアログによる採用決定をシステム上で行えるようにする。採用時に選択ORDを「発注済」、非選択ORDを「失注」に一括更新し、見積履歴を残す。
**現在のコード(修正対象)**: Phase 1で Read した内容を記載する。
- `000_infra/003_contracts.js` の `OrderDTO` 型定義(全プロパティ、`発注ステータス` の現在の選択肢、行番号付き)
- `100_config/101_sys_config.js` の `31_wrk_order` DDL定義箇所(行番号付き)
- `100_config/101_sys_config.js` の `onOpen()` メニュー定義箇所(行番号付き)
**修正方針**: 機能を「① 見積紐付け(案件ID採番・一括設定)」「② 比較ビュー表示(HTMLダイアログ)」「③ 採用決定とステータス一括更新」の3フェーズで設計する。
- **① 見積紐付け**
- ユーザーが `31_wrk_order` シートで「見積中」ステータスのORD行を1行以上選択し、メニューから実行すると起動
- Phase 1で確定した採番方式で `CASE_YYYYMMDD_NNNN` 形式のIDを生成し、選択行全ての「案件ID」列に一括書き込み
- `SpreadsheetApp.getActiveSheet().getActiveRangeList()` で選択行を取得し、各行のヘッダーインデックスで「発注ステータス」と「案件ID」を操作する(列番号ハードコード禁止)
- ガード: 選択行が0件、または「見積中」以外のステータス混在の場合は `Utils.toastResult('assignCaseId_', 'エラーメッセージ', 5)` で通知し中断
- **② 比較ビュー表示**
- ユーザーがいずれかのORD行を選択しメニューから実行すると、その行の「案件ID」を取得
- `OrderRepository.findAll()` で全ORDを取得し、同一「案件ID」を持つレコードをフィルタリング
- 主要項目(取引先名・契約件名・税込金額_発注・発注ステータス・摘要)をHTMLテーブル化し、Phase 1で確定したHTMLファイルのモーダルダイアログで表示
- 各行に「この見積を採用」ボタンを設置。クリック時にクライアントサイドで確認ダイアログ(`window.confirm('「[取引先名]」の見積を採用します。他の見積は失注になります。よろしいですか?')`)を表示し、OKなら `google.script.run.withSuccessHandler(onSuccess).withFailureHandler(onError).adoptQuote_(ordId, caseId)` を呼ぶ
- **③ 採用決定とステータス一括更新**
- `adoptQuote_(ordId, caseId)` サーバーサイド関数(引数: `ordId` = 採用するORDのID文字列、`caseId` = 案件ID文字列):
1. `OrderRepository.findAll()` で `{ headers, dtos }` を取得
2. 楽観的ロック検証: `caseId` と一致する全DTOの `発注ステータス` が「見積中」であることを確認。違反があれば `{ success: false, message: '...' }` を返しクライアントでエラー表示
3. 採用DTO(`発注ID(ORD)` === `ordId`): `発注ステータス` → "発注済"
4. 非採用DTO(同一`caseId`・採用以外): `発注ステータス` → "失注"
5. `OrderRepository.save(updatedDtos)` で全置換書き込み(`save` は `dtos` 全件を渡す)
6. `{ success: true }` を返しクライアントでダイアログを閉じる
- **DDL・DTO変更**
- `000_infra/003_contracts.js`: `OrderDTO` に `案件ID` プロパティを追加(**末尾追加**、`@property {string} 案件ID` 形式、初期値空文字)
- `100_config/101_sys_config.js`: `setupAllSchemas` の `31_wrk_order` DDL定義に `案件ID` 列を末尾追加(冪等性担保)、`発注ステータス` のデータ入力規則に "失注" を追加
- `100_config/101_sys_config.js`: `onOpen()` に新機能のメニュー項目を追加(Phase 1で確認した実在するメニュー名・形式に準拠)
**影響範囲**: Phase 1確定のファイルパス一覧、各ファイルの変更量目安、`OrderRepository.save()` が全置換方式であるため書き込み中の他操作との競合リスク
**注意事項**:
1. `OrderRepository.save(dtos)` は `writeDtosToSheet_` 経由の**全置換方式**。採用決定時は `findAll()` で取得した全DTOを更新して `save()` に渡すこと。`getRange().setValues()` 等のシート直接操作は禁止。
2. `31_wrk_order` のDDL列追加は**末尾追加**とすること。B列等の既存列中間への挿入は `Contracts.toDto` / `Contracts.toRow` のヘッダーインデックス対応により既存データが列ずれする恐れがあり禁止。
3. `google.script.run` 呼び出しには **`withFailureHandler`** を必ず設定すること。ネットワークエラーや例外がサイレントに失敗するのを防ぐ。
4. HTML内の採用確認は**クライアントサイドの `window.confirm()`** で実装すること。HTMLダイアログ表示中にサーバーサイドから `SpreadsheetApp.getUi().alert()` を呼ぶと干渉が発生するリスクがある。
5. `Utils.toastResult(funcName, message, duration)` の第1引数は呼び出し元の関数名(文字列)とすること(例: `'assignCaseId_'`)。
6. 列参照は必ず `headers.indexOf('列名')` でインデックスを取得すること。列番号ハードコード禁止。
7. `CASE_` 採番ロジックは Phase 1で確定した方式(`ID_PREFIX_MAP` 追加 or 独立ヘルパー)で実装し、GASの同時実行制限(最大30並列)により完全な排他制御は保証されないことを注意事項に明記する。
### Step 2-3a: エッジケース〜人間検討事項の追記 (Edit or Bash, ~200行)
**エッジケーステーブル**(以下の全条件を網羅すること):
| 条件 | 表示値・動作 | 理由 |
|------|-------------|------|
| 見積紐付け時に選択行が0件 | `Utils.toastResult` でエラー通知し中断 | ガード処理 |
| 選択行に「見積中」以外のステータス混在 | エラー通知し中断。処理対象行は「見積中」のみ | 意図しない再紐付け防止 |
| 選択行の「案件ID」が既に設定済み | 上書き確認ダイアログを表示し、OKなら上書き・キャンセルは中断 | 誤った案件統合の防止 |
| 比較ビュー実行時に対象行の「案件ID」が空 | 「案件IDが未設定です。先に見積紐付けを実行してください。」とエラー通知 | 操作順序の誘導 |
| 同一案件IDのORDが1件のみ | ダイアログを表示するが「比較対象が1件のみです」と注記表示 | 情報提供(処理は継続可能) |
| 採用決定時に対象ORDのステータスが「見積中」以外(楽観的ロック失敗) | `{ success: false }` を返却。クライアントで「他の操作により変更された可能性があります。画面を更新して再試行してください。」と表示し中断 | 二重採用・競合操作防止 |
| `OrderRepository.save()` 実行中の例外 | `Utils.logError` でスタックトレースをログ記録、`{ success: false, message: 'シートへの書き込みに失敗しました。' }` を返却 | エラー追跡可能性の担保 |
| 案件IDに紐付けられたORDが全て「見積中」以外に更新済み | 比較ダイアログで「採用済みまたは失注済みの見積です」と表示し、採用ボタンを非活性化 | 誤操作防止 |
**実データ検証**(実装前に確認すること):
- `31_wrk_order` の `発注ステータス` の現在DDLプルダウン選択肢と実際のシートデータを照合し、「見積中」の表記が完全一致していることを確認する(DDLコード値 vs 実データの乖離チェック、失敗パターン #3 の対策)。
- `setupAllSchemas` 実行後に `31_wrk_order` の末尾列として `案件ID` が追加され、既存データの列ずれが発生していないことを確認する。
**関連ドキュメント**:
| 仕様書 | 関連箇所 |
|--------|---------|
| MAS-125(発注ステータス遷移ワークフロー標準化) | 「失注」ステータス追加により「見積中→発注済」「見積中→失注」の2遷移パスが確定。MAS-125の設計前提に影響 |
| MAS-131(失注レコードアーカイブ) | 本案件でステータスを「失注」に変更。物理削除・アーカイブはMAS-131スコープ |
| MAS-133(関連削除ワークフロー) | 同上。レコード保持期間の定義はMAS-133スコープ |
**人間が検討すべき事項**:
- `docs/_internal/TODO_future.md` のMAS-129記載内容を転記する。
- 以下を追加で記載する:
- **「失注」ステータス追加とMAS-125への影響**: 本案件完了時点で `発注ステータス` の遷移パスが「見積中→発注済→検収済」「見積中→失注」の2系統に確定する。MAS-125のワークフロー設計はこの遷移パスを前提に設計すること。
- **失注見積の保持期間**: 本案件スコープではステータス「失注」への変更のみ実施。レコードのアーカイブ・物理削除はMAS-131・MAS-133で対応。
- **HTMLダイアログ方式の採用判断**: Phase 1で既存のHTMLサービス実装パターンを確認した結果を記載し、採用理由を明記する。
### Step 2-3b: 実装プロンプト〜変更履歴の追記 (Edit or Bash, ~250行)
**実装プロンプト(コードブロック禁止。行頭4スペースインデントで出力すること)**:
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-129「見積中ORDの複数見積比較機能」を実装してください。
## 実行前タスク
- `000_infra/003_contracts.js` の `OrderDTO` 型定義(全プロパティ・`発注ステータス` 選択肢)をReadで確認する。
- `200_data/202_repository.js` の `OrderRepository.findAll()` と `OrderRepository.save()` の実装をReadで確認する(save は writeDtosToSheet_ 経由の全置換方式であることを把握する)。
- `100_config/101_sys_config.js` の `31_wrk_order` DDL定義箇所(行番号付き)と `onOpen()` のメニュー定義(実在するメニュー名を確認)をReadで確認する。
- 新規実装ファイル([仕様書に記載のパス])とHTMLファイル([仕様書に記載のパス])の存在を確認し、なければ新規作成する。
## 修正対象ファイル
- `000_infra/003_contracts.js`: OrderDTO に `案件ID` プロパティを末尾追加
- `100_config/101_sys_config.js`: setupAllSchemas の31_wrk_order DDL末尾列追加 + 発注ステータスプルダウンに「失注」追加 + onOpen() にメニュー項目追加
- [仕様書に記載の新規実装ファイル]: assignCaseId_()・showQuoteComparison_()・adoptQuote_() の実装
- [仕様書に記載のHTMLファイル]: 比較ビューダイアログのHTMLテンプレート(google.script.run連携・withFailureHandler必須)
## 実装内容
仕様書 docs/dev/dev_mas-129_quote_comparison.md の「修正方針」セクションに記載した3フェーズ実装手順に従うこと。
## 制約
- OrderRepository.save(dtos) 以外でのシート直接書き込み(getRange / setValues)は禁止。
- 列番号ハードコード禁止。headers.indexOf('列名') でインデックスを取得すること。
- setupAllSchemas の変更は冪等性を担保すること(既存列が存在する場合はスキップ)。
- onOpen() への追加は、Readで確認した実在するメニュー名・構造に準拠すること(造語禁止)。
- Utils.toastResult(funcName, message, duration) の第1引数は呼び出し元の関数名(文字列)とすること。
- google.script.run 呼び出しには withFailureHandler を必ず設定すること。
- 採用確認はクライアントサイドの window.confirm() で実装すること(サーバーサイドからの SpreadsheetApp.getUi().alert() はHTMLダイアログ表示中に干渉するリスクがある)。
- 31_wrk_order のDDL列追加は末尾追加のみ。既存列の中間挿入は禁止(列ずれによるデータ破損防止)。
## エッジケース
仕様書のエッジケーステーブルに記載した全条件を実装すること。特に楽観的ロック(採用決定時の発注ステータス再検証)と withFailureHandler によるHTMLダイアログ内エラー表示を必ず実装する。
## 動作確認
1. npm run push:dev で開発環境にデプロイ
2. GASエディタから setupAllSchemas を実行し、31_wrk_order に「案件ID」列(末尾)と「失注」プルダウン選択肢が追加されていることをシート上で確認する
3. 31_wrk_order に「見積中」ステータスのORD行を2件以上手動追加し、見積紐付けメニューを実行 → 同一 CASE_YYYYMMDD_NNNN が全選択行に設定されることを確認する
4. 紐付け済みのORD行を選択し、比較ビューメニューを実行 → モーダルダイアログがHTMLテーブル形式で表示されることを確認する
5. 「この見積を採用」ボタンをクリック → window.confirm ダイアログが表示されることを確認する
6. 確認OKをクリック → 採用ORDが「発注済」、他ORDが「失注」に更新されることをシートで確認する
7. 楽観的ロックのテスト: 比較ダイアログを開いたまま別のシートタブで対象ORDのステータスを手動変更し、採用ボタンをクリックしてエラーメッセージが表示されることを確認する
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| Phase 1(ファイル調査・設計確定) | あり | ファイル名・関数名・行番号・列位置を全確定 |
| Phase 2(清書) | 最小限 | Phase 1確定内容の書き下しのみ |
| 実装フェーズ | あり(DDL変更・機能実装) | 挿入位置・パターン適用の判断 |
**推奨実行モデルテーブル**:
| 工程 | 推奨モデル | 理由 |
|------|----------|------|
| DDL変更(003_contracts・101_sys_config) | Sonnet | 既存パターンへの追記。挿入位置の判断が必要 |
| 機能実装(案件ID採番・紐付け・比較・採用) | Sonnet | 複数ファイル横断だが仕様書で設計確定済み |
| HTMLダイアログ実装 | Haiku | テンプレート的なHTML/JS実装。google.script.run連携パターンが既存にあれば踏襲 |
**変更履歴テーブル**:
| 日付 | 変更内容 |
|------|---------|
| 2026-04-20 | 初版作成 |
### Step 2-4: 仕様書作成プロンプトの記録 (Edit or Bash, 最重量・必ず独立Step)
末尾「## 仕様書作成プロンプト(再現性・監査性のため必ず記録)」セクションに以下の形式でこの `<instruction>` タグ内の全文を追記する:
<details><summary>展開して表示</summary>
[この instruction タグ内の全文をそのまま貼り付け]
</details>
---
## Phase 3: 保存・登録・コミット
### 3-A: `docs/_config.json` へのナビゲーション登録(必須)
`docs/_config.json` の `nav` 配列のうち、UIメニュー系新機能(MAS-094等が登録されているセクション)に以下を追加する(追加先セクションは `_config.json` を Read して MAS-094 の登録箇所を確認してから決定すること):
```json
{ "file": "dev/dev_mas-129_quote_comparison.md", "title": "E.X.X S-57 見積中ORDの複数見積比較機能" }
```
### 3-B: changelog への追記
`docs/_internal/changelog.md` の先頭(ヘッダー直後)に追記:
```
| 2026-04-20 | [dev_mas-129_quote_comparison.md](dev_mas-129_quote_comparison.md) | 初版作成。31_wrk_orderへの案件ID紐付け・HTMLダイアログ比較ビュー・採用決定(発注済/失注一括更新)の3フェーズ設計 |
```
### 3-C: コミット・プッシュ
```bash
git add docs/dev/dev_mas-129_quote_comparison.md docs/_config.json docs/_internal/changelog.md
git commit -m "docs: S-57 見積中ORDの複数見積比較機能の開発仕様書を作成
31_wrk_orderへの案件ID(CASE_)紐付け・HTMLダイアログによる横並び比較ビュー・
採用決定とステータス一括更新(発注済/失注)の3フェーズ設計。
S-53発注ステータス遷移ワークフローへの影響を明記。
https://claude.ai/code/session_XXXXX"
git push -u origin {現在のブランチ名}
```