MAS-015: リソース稼働率のヒートマップ可視化
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-015 |
| カテゴリ | FP&A・レポーティング / UX |
| Phase | P3 |
| 優先度 | ★ |
| 所要時間 | 3-4時間(Repository 新設 + マート 1 本 + 条件付き書式) |
| 対象ファイル | 000_infra/003_contracts.js(ResourceDTO 追記)200_data/202_repository.js(ResourceRepository 追記)100_config/101_sys_config.js(RSC_HEAT システムキー登録 + MENU_DEFINITION に 1 項目追加 — 002_constants.js)000_infra/002_constants.js(MENU_DEFINITION の 📋 サイドバー: 📊 マート更新 カテゴリに 1 項目追加)600_report/610_datamart_resource.js(新規作成) |
| 出力先タブ | 69_rsc_heatmap(動的生成・DDL 管理外) |
| データソース | 70_bud_resource(システムキー BUD_RSCE) |
| 一次資料 | docs/_internal/TODO_future.md §3.2 F-15 |
目的
70_bud_resource に月次で蓄積される要員稼働率データを集計し、新規タブ 69_rsc_heatmap にメンバー × 月のヒートマップ形式で出力する。閾値超過セルを条件付き書式で赤/黄着色することで、個別メンバーの過負荷 (>120%)・余力 (<100%)・空き月を一覧視覚化し、PM / PMO が月次でリソース配分を判断できるようにする。
一次資料(TODO_future.md)の期待効果:
- リソース配分の最適化
- 「稼働率の目標値(100% or 80%)」の設定判断材料
本案件は 既存 70_bud_resource の再集計レポート層の新設 のみで、70_bud_resource 自体のスキーマ変更・既存 420_project_profitability.js の計算ロジック変更は伴わない(S-33「工数入力化」とは独立)。
現在のコード
新規実装のため既存実装はない。関連する既存資産は以下の通り。
データソース: 70_bud_resource の実スキーマ(確定情報)
100_config/101_sys_config.js:837 の DDL 定義:
'BUD_RSCE': { headers: ["有効フラグ","要員名","PJ・案件名","対象年月","稼働率(%)"], color: "#e69138",
validations: {
"稼働率(%)": { type: 'range', min: 0, max: 1, helpText: '0〜1の小数で入力してください(例: 0.05 = 5%)' }
}
},
重要: 実スキーマは 5 列のみで、終了年月 列は存在しない。1 行 = 1 メンバーの 1 PJ・1 ヶ月の稼働率。対象年月 列が単一月を保持するため、本案件では期間展開 (開始〜終了の月別展開) は不要。同一メンバー × 同一月の複数 PJ 行は集計時に合算する。
値域: 稼働率(%) は 0.0〜1.0 の小数(1.0 = 100%)。見出しに (%) とあるが格納値は比率(decimal)。本仕様書の閾値・表示も同じスケールで扱う。
参考: 既存の集計実装(400_domain/420_project_profitability.js:73-86)
buildProjectProfitability() が 70_bud_resource を PJ 別労務費算出のために既に読み込んでいる。列名の参照方法(rsData[0].indexOf('要員名') 等)と有効フラグのスキップ処理はこのコードのパターンを踏襲する:
const idxName = rsData[0].indexOf('要員名'), idxPj = rsData[0].indexOf('PJ・案件名'),
idxYm = rsData[0].indexOf('対象年月'), idxRate = rsData[0].indexOf('稼働率(%)');
for(let i=1; i<rsData.length; i++){
const row = rsData[i];
if(row[0] === false || String(row[0]).toUpperCase() === 'FALSE') continue;
// ...
}
参考: 動的生成タブの先例(93_kpi_dashboard)
600_report/609_datamart_kpi.js:21-53 の buildKpiDashboard() は 93_kpi_dashboard を ss.insertSheet(name) で生成し毎回 sheet.clear() + sheet.clearConditionalFormatRules() してから再描画するパターン。setupAllSchemas() では 01_sys_config へのシステムキー登録のみ行い、DDL schemas マップには登録しない(= 動的生成扱い)。
参考: 条件付き書式の先例(600_report/609_datamart_kpi.js:476-522 / 600_report/606_datamart_daily_cf.js:178-184)
SpreadsheetApp.newConditionalFormatRule().whenNumberGreaterThan(x).setBackground('#F4CCCC').setFontColor('#CC0000').setRanges([range]).build() を配列で組み立て、sheet.setConditionalFormatRules(rules) で一括適用する。
修正方針
アーキテクチャ決定
- 新規タブ方式:
69_rsc_heatmapを新設する。番号帯 69 は「60 番台=財務帳票 (61-68) の直前」で、リソース系レポートとして空き番号。93_kpi_dashboardと同じく 動的生成タブ(DDL 管理外) として扱う。 - Repository 層追加(読み取り専用):
ResourceRepository.findAll()のみ実装する(save()/append()は本案件のスコープ外)。既存420_project_profitability.jsは直接getDataRange().getValues()で読み込んでいたが、本案件ではOrderRepositoryと同じ DTO 経由パターンで追加する(将来の再利用性のため)。 - 集計ロジックは合算のみ:
70_bud_resourceの各行は既に月粒度のため、{ 要員名: { 'YYYY-MM': totalRate } }のマップに加算していくだけ。期間展開 (addMonthsループ) は不要。 - 表示期間はパラメータ化:
Constants.getParam('HEATMAP_PAST_MONTHS', 6)とConstants.getParam('HEATMAP_FUTURE_MONTHS', 12)で過去月数・将来月数を外部設定化。基準は本日を含む月 (todayの YYYY-MM)。 - 月列は期間から算出:
getLastColumn()等の動的列範囲取得は禁止(failure_patterns.md #21 対策)。月列数 =pastMonths + 1 + futureMonthsを GAS 側で計算してからsetValues()する。 - 閾値・色分けは条件付き書式: セル値(合算稼働率)に対し
whenNumberGreaterThan(threshWarn)→ 赤、whenNumberGreaterThan(threshSafe)→ 黄 の 2 ルール。セル値自体の書き換えはしない(合算値をそのまま表示)。 - メニューは宣言的定義 (
MENU_DEFINITION) に追記:000_infra/002_constants.jsのMENU_DEFINITIONの📋 サイドバー: 📊 マート更新カテゴリに{ label: '🔥 リソース稼働率ヒートマップ更新', funcName: 'buildResourceHeatmap', description: '69 タブにメンバー × 月のヒートマップを再生成' }を追加する。ui.createMenuは直接編集しない(onOpen()L323 はMENU_DEFINITIONを走査して自動生成する実装のため)。
Step 1: ResourceDTO の追加(000_infra/003_contracts.js 末尾)
BudgetDTO と同一の @typedef スタイルで追加する。フィールド名は 100_config/101_sys_config.js:837 の DDL ヘッダーと完全一致させる:
/**
* 70_bud_resource — 要員稼働率(PJ別・月次)
* @typedef {Object} ResourceDTO
* @property {boolean} 有効フラグ
* @property {string} 要員名
* @property {string} PJ・案件名
* @property {Date|string} 対象年月 - "YYYY-MM" または Date 型(月初)
* @property {number} 稼働率(%) - 0.0〜1.0 の小数 (1.0 = 100%)
*/
Step 2: ResourceRepository の追加(200_data/202_repository.js 末尾)
OrderRepository のパターンを完全踏襲する。本案件では findAll() のみ実装(読み取り専用):
// =====================================================================
// ResourceRepository — 70_bud_resource (読み取り専用)
// =====================================================================
var ResourceRepository = {
/** @private */
_getSheet: function() {
// 既存の 420_project_profitability.js L16 と同じフォールバック順
var sheet = Utils.getSheetByKey('BUD_RSCE', '70_bud_resource');
if (sheet) return sheet;
return SpreadsheetApp.getActiveSpreadsheet().getSheetByName('27_bud_resource');
},
/**
* 全要員稼働率レコードを DTO 配列で取得する。
* @returns {{ headers: string[], dtos: ResourceDTO[] }}
*/
findAll: function() {
return readSheetAsDtos_(ResourceRepository._getSheet());
},
};
システムキー BUD_RSCE は 100_config/101_sys_config.js:781 で 01_sys_config に登録済み(DDL 既存エントリ)。第 2 引数のフォールバックは 70_bud_resource を優先し、旧タブ名 27_bud_resource も保険で試す(420 の既存実装に合わせる)。
Step 3: 100_config/101_sys_config.js の修正(setupAllSchemas() のシステムキー登録のみ)
setupAllSchemas() の confSheet.appendRow ブロック(L770-824)に 1 行追加する:
// 60番台レポート系の近くに挿入
if (!existKeys.includes('RSC_HEAT')) confSheet.appendRow(['RSC_HEAT', '', '69_rsc_heatmap', 'リソース稼働率ヒートマップ']);
schemas マップ(L826-)には登録しない。69_rsc_heatmap は DDL 管理外の動的生成タブとして扱い、buildResourceHeatmap() が毎回 clear() + 再構築する(93_kpi_dashboard / 76_notes / 72_bs_snap と同じ扱い)。これにより CLAUDE.md の「DDL (setupAllSchemas) で管理されないタブ」リストへの追記も必要(後述「関連ドキュメント」参照)。
Step 3-B: メニュー追加(000_infra/002_constants.js の MENU_DEFINITION)
MENU_DEFINITION 配列(L206-324)の 📋 サイドバー: 📊 マート更新 カテゴリ(L230-239)の items に 末尾追加 する:
{ label: '🔥 リソース稼働率ヒートマップ', funcName: 'buildResourceHeatmap', description: '69 タブにメンバー × 月の稼働率ヒートマップを再生成' },
onOpen()(100_config/101_sys_config.js:323)は Constants.MENU_DEFINITION を走査して ui.createMenu / addItem を自動生成するため、onOpen() 本体の編集は不要。
Step 4: 600_report/610_datamart_resource.js(新規作成)
/**
* =========================================================
* 610_datamart_resource.js — F-15: リソース稼働率ヒートマップ
* =========================================================
* 70_bud_resource(要員名 × PJ × 対象年月 × 稼働率)を集計し、
* 69_rsc_heatmap に「メンバー × 月」のヒートマップを出力する。
*
* - 月列: 本日月を基準に [過去 HEATMAP_PAST_MONTHS ヶ月 ~ 将来 HEATMAP_FUTURE_MONTHS ヶ月]
* - セル値: 合算稼働率 (0.0 以上の小数、1.2 超は過負荷)
* - 着色: 条件付き書式で >threshSafe 黄 / >threshWarn 赤
*/
function buildResourceHeatmap() {
var FUNC = 'buildResourceHeatmap';
try {
Utils.logInfo(FUNC, '処理開始');
var ss = getWebSpreadsheet_();
var sheetName = Utils.getSheetNameByKey('RSC_HEAT') || '69_rsc_heatmap';
var sheet = ss.getSheetByName(sheetName) || ss.insertSheet(sheetName);
sheet.clear();
sheet.clearConditionalFormatRules();
// (1) パラメータ取得
var pastMonths = Number(Constants.getParam('HEATMAP_PAST_MONTHS', 6));
var futureMonths = Number(Constants.getParam('HEATMAP_FUTURE_MONTHS', 12));
var threshSafe = Number(Constants.getParam('HEATMAP_THRESHOLD_SAFE', 1.0));
var threshWarn = Number(Constants.getParam('HEATMAP_THRESHOLD_WARNING', 1.2));
// (2) 月列ラベル生成(本日を含む月を基準)
var today = new Date();
var baseYm = Utilities.formatDate(today, 'JST', 'yyyy-MM');
var targetMonths = [];
for (var i = -pastMonths; i <= futureMonths; i++) {
targetMonths.push(Utils.addMonths(baseYm, i));
}
// (3) データ読み込み&合算
var result = ResourceRepository.findAll();
var rateMap = {}; // { 要員名: { 'YYYY-MM': 合算稼働率 } }
var memberSet = {}; // 順序保持用
result.dtos.forEach(function(dto) {
var flag = dto['有効フラグ'];
if (flag === false || String(flag).toUpperCase() === 'FALSE') return;
var name = String(dto['要員名'] || '').trim();
if (!name) return;
var ym = Utils.parseDateToYm(dto['対象年月']);
if (!ym || targetMonths.indexOf(ym) === -1) return;
var rate = Utils.parseAmt(dto['稼働率(%)']);
if (!rateMap[name]) { rateMap[name] = {}; memberSet[name] = true; }
rateMap[name][ym] = (rateMap[name][ym] || 0) + rate;
});
// (4) 出力配列組み立て
var memberNames = Object.keys(memberSet).sort();
var headerRow = ['要員名'].concat(targetMonths);
var dataRows = memberNames.map(function(name) {
var row = [name];
targetMonths.forEach(function(ym) {
row.push(rateMap[name][ym] || 0);
});
return row;
});
var outValues = [headerRow].concat(dataRows);
// (5) 書き込み
var rows = outValues.length;
var cols = headerRow.length;
sheet.getRange(1, 1, rows, cols).setValues(outValues);
// (6) 書式設定
sheet.getRange(1, 1, 1, cols).setBackground('#434343').setFontColor('#FFFFFF').setFontWeight('bold');
sheet.setFrozenRows(1);
sheet.setFrozenColumns(1);
if (dataRows.length > 0) {
sheet.getRange(2, 2, dataRows.length, targetMonths.length).setNumberFormat('0.0%;[Red]△ 0.0%;"-"');
}
sheet.getDataRange().setFontFamily('BIZ UDGothic');
sheet.autoResizeColumns(1, 1);
sheet.setColumnWidths(2, targetMonths.length, 80);
// 余分な列・行を削除
if (sheet.getMaxColumns() > cols) sheet.deleteColumns(cols + 1, sheet.getMaxColumns() - cols);
if (sheet.getMaxRows() > rows) sheet.deleteRows(rows + 1, sheet.getMaxRows() - rows);
// フィルター
var ef = sheet.getFilter(); if (ef) ef.remove();
if (rows > 1) sheet.getRange(1, 1, rows, cols).createFilter();
// (7) 条件付き書式
if (dataRows.length > 0) {
var dataRange = sheet.getRange(2, 2, dataRows.length, targetMonths.length);
var rules = [
// 過負荷 (>threshWarn, 例 120%) → 赤
SpreadsheetApp.newConditionalFormatRule()
.whenNumberGreaterThan(threshWarn)
.setBackground('#F4CCCC').setFontColor('#CC0000').setBold(true)
.setRanges([dataRange]).build(),
// 上限近傍 (>threshSafe, 例 100%) → 黄
SpreadsheetApp.newConditionalFormatRule()
.whenNumberGreaterThan(threshSafe)
.setBackground('#FCE5CD').setFontColor('#B45F06')
.setRanges([dataRange]).build(),
];
sheet.setConditionalFormatRules(rules);
}
Utils.logInfo(FUNC, sheetName + ' 再描画完了 (メンバー=' + memberNames.length + ', 月=' + targetMonths.length + ')');
} catch (e) {
Utils.logError(FUNC, e);
throw e;
}
}
影響範囲
| ファイル | 変更量 | 内容 |
|---|---|---|
000_infra/003_contracts.js | +8 行 | ResourceDTO の @typedef 追記(末尾) |
000_infra/002_constants.js | +1 行 | MENU_DEFINITION 📋 サイドバー: 📊 マート更新 に 1 項目追加 |
100_config/101_sys_config.js | +1 行 | setupAllSchemas() の appendRow ブロックに RSC_HEAT 登録 |
200_data/202_repository.js | +20 行 | ResourceRepository 新規追加(末尾、findAll のみ) |
600_report/610_datamart_resource.js | +130 行 | 新規ファイル |
CLAUDE.md | +1 行 | 「DDL (setupAllSchemas) で管理されないタブ」リストに 69_rsc_heatmap を追加 |
既存ファイルへの影響(確認済み・影響なし):
400_domain/420_project_profitability.js:70_bud_resourceを直接getDataRange()で読み込み続ける(PJ 別労務費算出は変更しない)。ResourceRepository経由に置換するかは将来検討事項(本案件のスコープ外)。600_report/601〜609_datamart_*.js: 変更なし。buildBudgetTrendDataMart()からの連鎖呼び出しも本案件では追加しない(ヒートマップは独立メニューから手動実行)。
注意事項
- 列参照はヘッダー名ベース。
DTO['要員名']/DTO['対象年月']等で参照する。getLastColumn()や列番号ハードコード禁止(CLAUDE.md コーディング規約)。 - 有効フラグ=FALSE の行は全処理でスキップする。
row[0] === false || String(row[0]).toUpperCase() === 'FALSE'の 2 条件判定を必ず実施(420_project_profitability.js:78の既存パターンに合わせる)。DTO['有効フラグ']経由でも同じ判定を行う。 - 動的列範囲取得禁止。月列数は
pastMonths + 1 + futureMonthsから算出してハードコードする。sheet.getLastColumn()は既存書式が残っていた場合に想定外の列数を返す(failure_patterns.md #21 対策)。 - 年月ラベル比較は正規化後に行う。
Utils.parseDateToYm()が返す値は"YYYY-MM"形式で確定しているが、targetMonthsと比較する前にUtils.parseDateToYm()を通してからindexOfする。文字列比較する場合は.replace(/[\s ]+/g, '')で全角スペース対策を行う(failure_patterns.md #22 対策)。 03_sys_paramsパラメータ参照はConstants.getParam()経由(PropertiesService直接呼び出し禁止)。初回呼び出し時にシートをスキャンしてキャッシュされるため、ヒートマップ再生成ごとのコストは低い。シートへ年月文字列を書く場合は apostrophe 前置またはsetNumberFormat('@')でテキスト化(failure_patterns.md #23 対策)。本案件ではtargetMonthsをヘッダー行に書くが、setValuesで書いた後に背景色等のみを設定し、自動で日付型解釈されないよう事前にsetNumberFormat('@')しておくと安全(上記コードの (5) と (6) の間にsheet.getRange(1, 2, 1, targetMonths.length).setNumberFormat('@')を追加する実装を推奨)。MENU_DEFINITION/setupAllSchemas()/onOpen()への追記は Read で実在コードを確認してから行う。記憶・Grep 部分ヒットだけで固有名詞(メニュー名・カテゴリ名・定数名・関数名)を書かない(failure_patterns.md #18-#20 対策)。本仕様書ではカテゴリ名📋 サイドバー: 📊 マート更新は000_infra/002_constants.js:230を Read して確定している。- 閾値のスケール:
70_bud_resourceの稼働率(%)は 0.0-1.0 の小数(DDL バリデーションmin: 0, max: 1)。閾値デフォルトHEATMAP_THRESHOLD_SAFE=1.0/HEATMAP_THRESHOLD_WARNING=1.2も同じスケール。「80% を標準とする運用」に切り替える場合はパラメータで 0.8 / 1.0 に再設定する(「人間が検討すべき事項」参照)。 - 数値フォーマット
0.0%: 0.0-1.0 を % 表示するためセル書式を0.0%;[Red]△ 0.0%;"-"にする。生の小数値を書き込んでいるため、ユーザーが手動で変更できる値は書式だけで、条件付き書式の閾値は小数の生値(1.0 = 100%)で比較する。 - 同一メンバー × 同一月の複数 PJ 行は合算する。例:
山田がPJ-A=0.5とPJ-B=0.7を同月に持つ場合、ヒートマップのセル値は1.2(= 120%)になる。PJ 別内訳を表示しない設計は意図的(過負荷の総合判定が目的)。PJ 別内訳が必要な場合は将来追加案件で扱う(「人間が検討すべき事項」参照)。
エッジケース
| 条件 | 表示値 / 動作 | 理由 |
|---|---|---|
70_bud_resource にデータ行がない(または有効フラグ=FALSE のみ) | 69_rsc_heatmap にヘッダー行(要員名 + 月列)のみ出力して正常終了。条件付き書式もスキップ | NPE・空 setRanges エラー防止。dataRows.length === 0 で 2 段目以降の書き込みをスキップ |
| 稼働率が数値でない(文字列混入・空欄) | Utils.parseAmt() で 0 として扱い、合算に 0 加算 | データ品質保護。parseAmt は typeof val === 'number' 判定で既存の小数値は保持、空欄・非数値は 0 に |
Utils.parseDateToYm() が "" を返す(年月不正) | そのレコードを集計から除外 (!ym チェック) | 不正データのマップ混入防止 |
対象年月 が表示期間外 (targetMonths に含まれない) | そのレコードを集計から除外 (indexOf === -1 チェック) | 列の膨張防止。古い過去月や遠い将来月のデータは捨てる |
要員名が空文字 (.trim() === '') | そのレコードを集計から除外 | ヒートマップ行が空名で埋まるのを防止 |
| 同一メンバー × 同一月の複数 PJ 行 | 合算して 1 セルに 0.5 + 0.7 = 1.2 のように格納。閾値超で着色 | 過負荷の総合判定が本案件の目的 |
| 合算稼働率が 1.0(100%)ぴったり | whenNumberGreaterThan(threshSafe=1.0) は不等号のため着色されない(白) | 「ちょうど 100%」は理想配分として許容。過負荷のみ検出する仕様 |
| 合算稼働率が 1.2(120%)ぴったり | whenNumberGreaterThan(threshWarn=1.2) は不等号のため赤着色されない(黄のまま) | 黄と赤の境界は「Warning 閾値を越えた瞬間から赤」を厳密適用 |
| 合算稼働率が 0(アサイン無し月) | セル値 0 を書き込み。書式 "-" により - で表示(0.0%;[Red]△ 0.0%;"-" の third part が適用) | 空き月を明示。null/空白より視認性が高い |
Constants.getParam() が未設定で defaultVal を返す | デフォルト pastMonths=6, futureMonths=12, threshSafe=1.0, threshWarn=1.2 で動作 | 初回パラメータ未設定でも実用上動作する |
69_rsc_heatmap タブが既に存在する | sheet.clear() + clearConditionalFormatRules() してから再描画 | 冪等性確保。前回の余分な行・列・書式も deleteRows/deleteColumns で物理削除 |
RSC_HEAT システムキーが 01_sys_config 未登録 | Utils.getSheetNameByKey が未定義を返し、フォールバック '69_rsc_heatmap' でタブ名を解決。存在しなければ ss.insertSheet(name) で新設 | setupAllSchemas() 未実行でも動作するよう保険を掛ける |
70_bud_resource が未作成(シート自体が存在しない) | ResourceRepository._getSheet() は 27_bud_resource フォールバックも試し、最終的に null を返す。readSheetAsDtos_ は { headers: [], dtos: [] } を返すため、ヘッダーのみのヒートマップが出力される | NPE 防止。サンドボックス環境でも落ちない |
実データ検証
70_bud_resource の実スキーマ確認(Phase 1 完了事項)
| 確認項目 | 確認結果 | 情報源 |
|---|---|---|
| DDL ヘッダー定義 | ["有効フラグ","要員名","PJ・案件名","対象年月","稼働率(%)"](5 列) | 100_config/101_sys_config.js:837 |
| メンバー名列 | 要員名(列 B) | 同上 |
| 期間列 | 対象年月(列 D)単一月。終了年月 は存在しない | 同上。docs/dev/dev_S-33_workforce_costing.md:116 も同定義を記載 |
| 稼働率列 | 稼働率(%)(列 E)、値域 0.0〜1.0 の小数 | DDL validations: { "稼働率(%)": { type: 'range', min: 0, max: 1 } } |
| ID 列 | 列 C(= PJ・案件名 の前に ID 列がない DDL 定義)。※PJ・案件名 列の後に 300_ui/301_ui_assist.js:120 の smartAddRow で RSC_NNNN を自動採番するロジックあり。DDL ヘッダーに ID 列は明示されていない(列 C に入る可能性あり・要 MCP 確認) | 002_constants.js:105 { pattern: '70_bud_resource', prefix: 'RSC_', digit: 4, isDate: false } |
| 有効フラグ列 | 列 A(全予算系シート共通)。row[0] で判定 | 同 DDL 定義 + 420_project_profitability.js:78 |
SHEET_DEFAULTS の _dynamic.開始年月 | DDL ヘッダー 対象年月 との齟齬あり(002_constants.js:78 に '開始年月': 'nextYm' と書かれているが実ヘッダーは 対象年月)。本案件では使用しない(smartAddRow の自動デフォルト値用。実データの列名は DDL 定義の 対象年月 に従う) | docs/dev/dev_S-33_workforce_costing.md:110 でも同齟齬を既知として記載済 |
実装前に MCP 実行すべき確認項目
70_bud_resourceシートの実際の 1 行目を取得し、DDL 定義と一致するか照合する(DDL 未実行環境で旧列名が残留していないか)。01_sys_configのsystemkey_gas列にBUD_RSCEとRSC_HEATが登録されているか確認する(RSC_HEATは本案件で新規追加)。03_sys_paramsの既存キー一覧を取得し、HEATMAP_*で始まるキーの重複がないことを確認する(本案件で新規キー 4 つを追加:HEATMAP_PAST_MONTHS/HEATMAP_FUTURE_MONTHS/HEATMAP_THRESHOLD_SAFE/HEATMAP_THRESHOLD_WARNING)。
03_sys_params 新規パラメータ(本案件で追加)
| キー | デフォルト値 | 用途 |
|---|---|---|
HEATMAP_PAST_MONTHS | 6 | 基準月から過去何ヶ月を表示するか |
HEATMAP_FUTURE_MONTHS | 12 | 基準月から将来何ヶ月を表示するか |
HEATMAP_THRESHOLD_SAFE | 1.0 | 黄色着色の下限閾値(これを超えると黄色) |
HEATMAP_THRESHOLD_WARNING | 1.2 | 赤着色の下限閾値(これを超えると赤) |
Constants.getParam() は defaultVal 引数の型に応じて Number() / String() 変換するため、数値で取得される(本コードでは Number() で明示変換)。
関連ドキュメント
| 仕様書 | 関連箇所 |
|---|---|
| CLAUDE.md | 変更時の動作確認テスト: 600_report/6*_datamart_*.js → マート更新テスト。コーディング規約(ヘッダー名ベース列参照・有効フラグ FALSE 行スキップ) |
| docs/dev/dev_S-33_workforce_costing.md | 70_bud_resource の「稼働率 (%)」→「工数 (h)」置換案件。S-33 実施後は本仕様の DTO['稼働率(%)'] 参照を調整する必要がある(依存関係: S-33 が本案件より先に入った場合、集計ロジックを「工数 / 基準工数 (例 160h)」で比率化するよう修正する) |
| docs/dev/dev_F-03_kpi_dashboard.md | 動的生成タブ・条件付き書式・Constants.getParam の先例 |
| docs/dev/dev_F-12_headcount_simulation.md | MENU_DEFINITION への項目追加パターン・ss.insertSheet() の動的タブ生成パターン |
| docs/_internal/failure_patterns.md #18-#20 | 仕様書記述時の「Read で実在コードを確認」原則(メニュー名・定数名・シート名) |
| docs/_internal/failure_patterns.md #21-#23 | getLastColumn() 禁止、全角スペース対策、年月文字列の apostrophe 前置 |
| docs/_internal/TODO_future.md §3.2 F-15 | 案件定義。期待される効果「リソース配分の最適化」、検討事項「稼働率の目標値(100% or 80%を標準とするか)」 |
CLAUDE.md の「DDL (setupAllSchemas) で管理されないタブ」リスト(CLAUDE.md 末尾近辺)に 69_rsc_heatmap を追記する必要がある。現在の記載は「03_sys_params, 75_ss_equity_changes, 76_notes, 77_pj_raw, 78_pj_pl, 91_fs_bs, 92_fs_pl, 93_kpi_dashboard, 90_test_results」(参考列挙。実際の記載は CLAUDE.md を Read して書き換えること)。
人間が検討すべき事項
- 稼働率の目標値(100% or 80% を標準とするか) — TODO_future.md 原文。
- 100% 基準:
HEATMAP_THRESHOLD_SAFE=1.0,HEATMAP_THRESHOLD_WARNING=1.2(本仕様書のデフォルト)。「100% を正常、120% 超は過負荷」。 - 80% 基準:
HEATMAP_THRESHOLD_SAFE=0.8,HEATMAP_THRESHOLD_WARNING=1.0。「80% を理想、100% 超は黄、120% 超は赤」のように 2 段スライド可能(ただし実装ではHEATMAP_THRESHOLD_WARNINGを 1.0 にするとほぼ全月が赤になるため、黄+赤境界を別途調整)。 - 推奨初期値: 100% 基準で運用開始し、1-2 ヶ月運用後に「余力可視化が弱い」と感じたら 80% 基準に切り替える(パラメータ変更のみで済む)。
- 100% 基準:
HEATMAP_PAST_MONTHS/HEATMAP_FUTURE_MONTHSのデフォルト値 — 本仕様書は 6/12 を採用。「過去半年 + 将来 1 年」は四半期/半期の採用計画レビューに合う粒度。採用計画の粒度が四半期主体であれば 3/9 に縮小することも検討。- PJ 別内訳の表示有無 — 本仕様書は「メンバー × 月」のみ。PJ 別分解(例:
山田/PJ-A,山田/PJ-Bを別行で表示)の要望があれば、別案件(F-15 Phase 2 相当)として切り出す。現状の合算表示は「誰が過負荷か」の判定に特化しており、誰が何に時間を使っているかの可視化には追加実装が必要。 69_rsc_heatmapを DDL 管理タブにするか、動的生成タブにするか — 本仕様書は 動的生成(93_kpi_dashboardパターン)を採用。理由は (a) 月列がHEATMAP_PAST_MONTHS/HEATMAP_FUTURE_MONTHSの設定で可変のため DDL 固定ヘッダーに馴染まない、(b) メンバー数が変動する。固定ヘッダーで管理したい場合はsetupAllSchemas()のschemasマップに追加する(その場合は月列が固定範囲、かつメンバー行のフィルタで絞り込む設計に変更)。420_project_profitability.jsのResourceRepository化 — 本案件では 420 は変更しない。将来的に全 Repository で DTO 経由の読み取りに統一するなら、別リファクタ案件として切り出す。S-33(工数入力化)と合わせて実施すると効率的。buildBudgetTrendDataMart()との連携有無 — 本仕様書は独立メニューから手動実行のみ。マート更新 (buildBudgetTrendDataMart) 末尾から自動呼び出しする設計も可能だが、ヒートマップは「リソース計画の変更時にレビューする」用途のため頻度が低く、自動連鎖させるとマート更新時間を延ばす。独立呼び出しを推奨。
実装プロンプト(Claude Code 用)
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 F-15「リソース稼働率のヒートマップ可視化」を実装してください。
大規模実装のため以下の 4 Step に分けて実施してください。
## 実行前タスク(各ファイルで確認すべきポイントを明記)
**Grep は「どこにあるか」の発見まで。型・固有名詞・呼び出し経路の確認は必ず Read で行う(failure_patterns.md #18-#20 対策)。**
- `000_infra/002_constants.js`:
- `SHEET_DEFAULTS` の `70_bud_resource` エントリ (L78) — `{ pattern, prefix, defaults }` 構造と prefix `RSC_` を確認
- `ID_PREFIX_MAP` の `70_bud_resource` エントリ (L105) — `digit: 4, isDate: false`
- `Constants.getParam(key, defaultVal)` (L147) — 03_sys_params からの読み込み仕様、defaultVal 型で Number/String 変換
- `MENU_DEFINITION` の `📋 サイドバー: 📊 マート更新` カテゴリ (L230-239) — このカテゴリの items 末尾にメニュー 1 項目を追加する
- `000_infra/003_contracts.js`:
- `BudgetDTO` の `@typedef` 形式を Read — `ResourceDTO` の雛形として使用
- `Contracts.toDto` ファクトリ — ヘッダー配列 + 行配列から DTO を生成
- `000_infra/004_utils.js`:
- `Utils.parseDateToYm(val)` (L92) — Date 型・"YYYY-MM"・"YYYY/MM"・"YYYY年MM月" 対応、パース不可は `""`
- `Utils.addMonths(ymStr, months)` (L127) — "YYYY-MM" の月加減算
- `Utils.parseAmt(val)` (L191) — 非数値は 0
- `Utils.getSheetByKey(key, fallbackName)` (L40) — `Utils.getSheetNameByKey` → `ss.getSheetByName(fallback)` の順
- `200_data/202_repository.js`:
- `readSheetAsDtos_` (L19) — 内部ヘルパー `{ headers, dtos }` を返す
- `OrderRepository` (L107-146) — `_getSheet` / `findAll` / `save` / `append` の雛形として完全踏襲(本案件は `findAll` のみ実装)
- `100_config/101_sys_config.js`:
- `setupAllSchemas()` (L749-) 内の `confSheet.appendRow` ブロック (L770-824) — ここに `RSC_HEAT` システムキー登録行を追加
- `schemas` マップ (L826-) の `'BUD_RSCE'` エントリ (L837) — DDL ヘッダー `["有効フラグ","要員名","PJ・案件名","対象年月","稼働率(%)"]` を確認。**`69_rsc_heatmap` はこの schemas マップには追加しない**(動的生成タブ扱い)
- `onOpen()` (L323) — `Constants.MENU_DEFINITION` を走査する実装。**`onOpen` 本体は編集不要**。メニュー追加は `000_infra/002_constants.js` の `MENU_DEFINITION` のみ修正
- `400_domain/420_project_profitability.js`:
- L73-86 の `70_bud_resource` 既存読み込みロジック(有効フラグ判定 + 列インデックス取得パターン)を参考にする
- `600_report/609_datamart_kpi.js`:
- L21-53 の `buildKpiDashboard()` — 動的生成タブの `clear + clearConditionalFormatRules + 再描画` パターン
- L476-522 の `applyKpiConditionalFormat_` — `SpreadsheetApp.newConditionalFormatRule()` の連鎖記法・`setConditionalFormatRules()` 一括適用
- `600_report/606_datamart_daily_cf.js`:
- L178-184 — `whenNumberLessThan` での条件付き書式。`setRanges([sheet.getRange(...)])` のパターン
- MCP で以下を確認する:
1. `70_bud_resource` の実ヘッダー(`対象年月` が単一月であること・`終了年月` が存在しないこと)
2. `01_sys_config` の既存システムキー(`RSC_HEAT` の重複がないこと)
3. `03_sys_params` の既存キー一覧(`HEATMAP_*` キーの重複がないこと)
## 修正対象ファイル
- `000_infra/003_contracts.js`(`ResourceDTO` の `@typedef` 追記)
- `000_infra/002_constants.js`(`MENU_DEFINITION` `📋 サイドバー: 📊 マート更新` カテゴリに 1 項目追加)
- `200_data/202_repository.js`(`ResourceRepository` 新設・末尾追記)
- `100_config/101_sys_config.js`(`setupAllSchemas()` の `appendRow` ブロックに `RSC_HEAT` 登録)
- `600_report/610_datamart_resource.js`(新規作成)
- `CLAUDE.md`(「DDL (setupAllSchemas) で管理されないタブ」リストに `69_rsc_heatmap` 追記)
## Step 1: ResourceDTO の追加
`000_infra/003_contracts.js` の末尾(`BudgetDTO` の `@typedef` の直後、または `PartnerDTO` のさらに下の適切な位置)に追記する。
フィールド名は MCP で確認した `70_bud_resource` の実ヘッダー名を使用すること(推測・造語禁止):
```js
/**
* 70_bud_resource — 要員稼働率(PJ別・月次)
* @typedef {Object} ResourceDTO
* @property {boolean} 有効フラグ
* @property {string} 要員名
* @property {string} PJ・案件名
* @property {Date|string} 対象年月
* @property {number} 稼働率(%) - 0.0〜1.0 の小数 (1.0 = 100%)
*/
```
## Step 2: ResourceRepository の追加
`200_data/202_repository.js` の末尾に追記する。`OrderRepository` パターンを完全に踏襲する:
```js
// =====================================================================
// ResourceRepository — 70_bud_resource (読み取り専用)
// =====================================================================
var ResourceRepository = {
/** @private */
_getSheet: function() {
var sheet = Utils.getSheetByKey('BUD_RSCE', '70_bud_resource');
if (sheet) return sheet;
return SpreadsheetApp.getActiveSpreadsheet().getSheetByName('27_bud_resource');
},
/**
* 全要員稼働率レコードを DTO 配列で取得する。
* @returns {{ headers: string[], dtos: ResourceDTO[] }}
*/
findAll: function() {
return readSheetAsDtos_(ResourceRepository._getSheet());
},
};
```
本案件は読み取り専用のため `save()` / `append()` は実装しない。
## Step 3-A: setupAllSchemas() のシステムキー登録
`100_config/101_sys_config.js` の `setupAllSchemas()` 内、60 番台レポートタブの `appendRow` ブロック(`PL_VAR` 登録 L795 付近)の近くに 1 行追加する:
```js
if (!existKeys.includes('RSC_HEAT')) confSheet.appendRow(['RSC_HEAT', '', '69_rsc_heatmap', 'リソース稼働率ヒートマップ']);
```
**`schemas` マップには追加しない**(動的生成タブ扱い。`93_kpi_dashboard` と同じパターン)。
## Step 3-B: MENU_DEFINITION への項目追加
`000_infra/002_constants.js` の `MENU_DEFINITION` 配列、`category: '📋 サイドバー: 📊 マート更新'` のブロックの `items` 末尾に 1 項目追加する:
```js
{ label: '🔥 リソース稼働率ヒートマップ', funcName: 'buildResourceHeatmap', description: '69 タブにメンバー × 月の稼働率ヒートマップを再生成' },
```
`onOpen()` は編集不要(`MENU_DEFINITION` を走査する実装のため自動反映)。
## Step 4: buildResourceHeatmap() の実装
`600_report/610_datamart_resource.js` を新規作成する。実装の骨格は仕様書 §修正方針 Step 4 を参照:
```js
function buildResourceHeatmap() {
var FUNC = 'buildResourceHeatmap';
try {
Utils.logInfo(FUNC, '処理開始');
var ss = getWebSpreadsheet_();
var sheetName = Utils.getSheetNameByKey('RSC_HEAT') || '69_rsc_heatmap';
var sheet = ss.getSheetByName(sheetName) || ss.insertSheet(sheetName);
sheet.clear();
sheet.clearConditionalFormatRules();
var pastMonths = Number(Constants.getParam('HEATMAP_PAST_MONTHS', 6));
var futureMonths = Number(Constants.getParam('HEATMAP_FUTURE_MONTHS', 12));
var threshSafe = Number(Constants.getParam('HEATMAP_THRESHOLD_SAFE', 1.0));
var threshWarn = Number(Constants.getParam('HEATMAP_THRESHOLD_WARNING', 1.2));
var baseYm = Utilities.formatDate(new Date(), 'JST', 'yyyy-MM');
var targetMonths = [];
for (var i = -pastMonths; i <= futureMonths; i++) {
targetMonths.push(Utils.addMonths(baseYm, i));
}
var result = ResourceRepository.findAll();
var rateMap = {}, memberSet = {};
result.dtos.forEach(function(dto) {
var flag = dto['有効フラグ'];
if (flag === false || String(flag).toUpperCase() === 'FALSE') return;
var name = String(dto['要員名'] || '').trim();
if (!name) return;
var ym = Utils.parseDateToYm(dto['対象年月']);
if (!ym || targetMonths.indexOf(ym) === -1) return;
var rate = Utils.parseAmt(dto['稼働率(%)']);
if (!rateMap[name]) { rateMap[name] = {}; memberSet[name] = true; }
rateMap[name][ym] = (rateMap[name][ym] || 0) + rate;
});
var memberNames = Object.keys(memberSet).sort();
var headerRow = ['要員名'].concat(targetMonths);
var dataRows = memberNames.map(function(name) {
var row = [name];
targetMonths.forEach(function(ym) { row.push(rateMap[name][ym] || 0); });
return row;
});
var outValues = [headerRow].concat(dataRows);
var rows = outValues.length, cols = headerRow.length;
// 月ラベル列を事前にテキスト形式に (failure_patterns #23 対策)
sheet.getRange(1, 2, 1, targetMonths.length).setNumberFormat('@');
sheet.getRange(1, 1, rows, cols).setValues(outValues);
sheet.getRange(1, 1, 1, cols).setBackground('#434343').setFontColor('#FFFFFF').setFontWeight('bold');
sheet.setFrozenRows(1);
sheet.setFrozenColumns(1);
if (dataRows.length > 0) {
sheet.getRange(2, 2, dataRows.length, targetMonths.length)
.setNumberFormat('0.0%;[Red]△ 0.0%;"-"');
}
sheet.getDataRange().setFontFamily('BIZ UDGothic');
sheet.autoResizeColumns(1, 1);
sheet.setColumnWidths(2, targetMonths.length, 80);
if (sheet.getMaxColumns() > cols) sheet.deleteColumns(cols + 1, sheet.getMaxColumns() - cols);
if (sheet.getMaxRows() > rows) sheet.deleteRows(rows + 1, sheet.getMaxRows() - rows);
var ef = sheet.getFilter(); if (ef) ef.remove();
if (rows > 1) sheet.getRange(1, 1, rows, cols).createFilter();
if (dataRows.length > 0) {
var dataRange = sheet.getRange(2, 2, dataRows.length, targetMonths.length);
var rules = [
SpreadsheetApp.newConditionalFormatRule()
.whenNumberGreaterThan(threshWarn)
.setBackground('#F4CCCC').setFontColor('#CC0000').setBold(true)
.setRanges([dataRange]).build(),
SpreadsheetApp.newConditionalFormatRule()
.whenNumberGreaterThan(threshSafe)
.setBackground('#FCE5CD').setFontColor('#B45F06')
.setRanges([dataRange]).build(),
];
sheet.setConditionalFormatRules(rules);
}
Utils.logInfo(FUNC, sheetName + ' 再描画完了 (メンバー=' + memberNames.length + ', 月=' + targetMonths.length + ')');
} catch (e) {
Utils.logError(FUNC, e);
throw e;
}
}
```
## Step 5: CLAUDE.md 追記
CLAUDE.md 末尾近辺の「## DDL (setupAllSchemas) で管理されないタブ」セクションに `69_rsc_heatmap` を追加する(既存の列挙順に合わせて挿入):
```
動的に生成・上書きされるタブ:
03_sys_params, 69_rsc_heatmap, 75_ss_equity_changes, 76_notes,
77_pj_raw, 78_pj_pl, 91_fs_bs, 92_fs_pl, 93_kpi_dashboard, 90_test_results
```
## 制約
- 列参照はヘッダー名ベース(`DTO['要員名']` / `indexOf` 等)。列番号ハードコード禁止(CLAUDE.md 規約)
- 有効フラグ列が `FALSE` の行は集計から除外(`row[0] === false || String(row[0]).toUpperCase() === 'FALSE'` の 2 条件判定)
- `getLastColumn()` での動的列範囲取得禁止(月列は `pastMonths + 1 + futureMonths` から算出)
- ラベル文字列の正規化は `.replace(/[\s ]+/g, '')` で全角スペースも除去
- 月ラベル(`targetMonths`)をヘッダー行に書く前にその範囲を `setNumberFormat('@')` でテキスト型に固定(`-` 記号を減算演算子として誤解釈されるのを防ぐ)
- 既存の `601〜609_datamart_*.js` / `420_project_profitability.js` は変更しない
- `MENU_DEFINITION` / `setupAllSchemas()` / `onOpen()` への追記は必ず Read で実在コードを確認してから行う(記憶・推測禁止)
## エッジケース
仕様書 §エッジケース テーブル全件を実装でカバーすること(データ 0 件・不正値・表示期間外・合算 >100% 等)。
## 実データ検証
実装前に MCP で以下を確認する:
- `70_bud_resource` の実ヘッダー列名(ResourceDTO フィールド確定、`終了年月` が無いことの再確認)
- `01_sys_config` のシステムキー(`RSC_HEAT` 重複なし)
- `03_sys_params` の既存キー一覧(`HEATMAP_*` 重複なし)
## 動作確認
1. `npm run push:dev` でデプロイする
2. GAS エディタで `setupAllSchemas()` を実行し、`01_sys_config` に `RSC_HEAT` / `69_rsc_heatmap` / `リソース稼働率ヒートマップ` が登録されることを確認
3. スプレッドシートを再読み込みし、`📋 サイドバー: 📊 マート更新` サブメニュー(または対応する統合サイドバー)に「🔥 リソース稼働率ヒートマップ」が出現することを確認
4. GAS エディタで `buildResourceHeatmap()` を直接実行する
5. `69_rsc_heatmap` シートが生成(またはクリア&再作成)され、列 A=要員名、列 B 以降=月列、データセル=合算稼働率(0.0〜1.0+ の小数、書式は `0.0%`)になっていること
6. `03_sys_params` に `HEATMAP_PAST_MONTHS`=6 / `HEATMAP_FUTURE_MONTHS`=12 / `HEATMAP_THRESHOLD_SAFE`=1.0 / `HEATMAP_THRESHOLD_WARNING`=1.2 を追加し、再実行後に値に応じた着色となることを確認
7. `70_bud_resource` に同一メンバー × 同一月の複数 PJ 行を追加し(例: `山田` × `PJ-A` × `2026-05` × `0.5` と `山田` × `PJ-B` × `2026-05` × `0.7`)、セル値が合算値 1.2 になり **黄色着色**されることを確認
8. さらに稼働率 1.3 のレコードを追加し、該当セルが **赤+太字着色**されることを確認
9. `70_bud_resource` のデータ行をすべて論理削除(有効フラグ=FALSE)した状態で実行し、`69_rsc_heatmap` がヘッダー行のみ出力され、条件付き書式がセットされない(エラーにならない)ことを確認
10. 表示期間外のレコード(例: `2020-01` のデータ)が `69_rsc_heatmap` に出現しないことを確認
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| Step 1: ResourceDTO 追加 | なし | DDL 確認済みフィールドの転記 |
| Step 2: ResourceRepository 追加 | なし | OrderRepository パターンの完全踏襲 |
| Step 3-A: setupAllSchemas に 1 行追加 | なし | 挿入位置は同族 `PL_VAR` 近傍 |
| Step 3-B: MENU_DEFINITION に 1 項目追加 | なし | カテゴリと items 位置は事前確定 |
| Step 4: buildResourceHeatmap() 実装 | あり | 月列生成・合算集計・条件付き書式の 3 ブロックの相互接続 |
| Step 5: CLAUDE.md 追記 | なし | 既存リストへの 1 語追加 |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| 仕様書作成(本ドキュメント) | Claude Opus 4.6 | 実データと SHEET_DEFAULTS の齟齬(終了年月 列の不在)を発見し、開始年月/終了年月 前提の task プロンプトを再設計する判断が必要 |
| Step 1: ResourceDTO 追加 | Claude Haiku 4.5 | DDL 確認済みフィールドの転記のみ。判断要素なし |
| Step 2: ResourceRepository 追加 | Claude Haiku 4.5 | OrderRepository パターンの完全踏襲。判断要素なし |
| Step 3-A: setupAllSchemas 追加 | Claude Haiku 4.5 | 1 行挿入。PL_VAR 近傍に配置するだけ |
| Step 3-B: MENU_DEFINITION 追加 | Claude Haiku 4.5 | 1 項目追加。カテゴリと位置は仕様書で確定済み |
| Step 4: buildResourceHeatmap() 実装 | Claude Sonnet 4.6 | 月列生成・合算・条件付き書式の組合せ。既存パターンの応用だが書式化・エッジケースの判断が必要 |
| Step 5: CLAUDE.md 追記 | Claude Haiku 4.5 | 既存リストに 1 語追加 |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-21 | 初版作成。70_bud_resource は 1 行 1 月の 対象年月 スキーマ(終了年月 不存在)であることを Phase 1 で確認し、期間展開不要の単純合算ロジックで設計。動的生成タブ(93_kpi_dashboard パターン)として 69_rsc_heatmap を新設。ResourceDTO / ResourceRepository(読み取り専用)・MENU_DEFINITION 追記・600_report/610_datamart_resource.js 新規の 5 ファイル構成。閾値は 03_sys_params の HEATMAP_* 4 キーで外部設定化(デフォルト: 過去 6 ヶ月・将来 12 ヶ月・黄 100%・赤 120%)。failure_patterns #18-#23 の直接対策(Read による固有名詞確認・getLastColumn 禁止・全角スペース正規化・setNumberFormat('@') による年月文字列の保護)を注意事項に明記 |
仕様書作成プロンプト
展開して表示
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**: Phase 1(設計)では拡張思考をフル活用し、ファイル名形式・エッジケース一覧・Step 分割粒度・固有名詞(関数名/シート名/列名/システムキー文字列)を完全に確定させる。Phase 2(清書)の各 Step 内では拡張思考を最小限に抑え、Phase 1 で確定済みの内容の書き下しに徹する。出力途中で再考しない。
2. **テキスト報告の禁止**: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
3. **4-5 分割の Write/Edit 実行**: 仕様書作成は以下の Step に分けて実行する: 2-1 骨格 Write(~20行)/ 2-2 概要〜注意事項 Edit/Bash(~300行)/ 2-3a エッジケース〜人間検討事項 Edit/Bash(~200行)/ 2-3b 実装プロンプト〜変更履歴 Edit/Bash(~250行)/ 2-4 `<details>` にプロンプト全文記録 Edit/Bash(最重量・必ず独立 Step)。1 回の Write/Edit は約 300 行以内を目安にする。
4. **各 Step で何を書くかを具体指示**: 設計判断を Phase 2 実行時に持ち込まないよう、各 Step の内容を Phase 1 で完全確定させてから書き出しに移る。
======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 F-15「リソース稼働率のヒートマップ可視化」の開発仕様書を作成してください。
作成後は `docs/_config.json` の `nav` 配列の §E.5(FP&A・レポーティング)セクションに必ず追記してください。
---
## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)
以下のファイルをすべて Read し、Phase 2 で使う固有名詞・型・行番号を確定させる。
**Grep は「どこにあるか」の発見まで。型・固有名詞・呼び出し経路の確認は必ず Read で行う。推測で仕様書に固有名詞を書かない(failure_patterns.md #18–#20 の直接対策)。**
### 1-A: 案件定義の読み込み
- `docs/_internal/TODO_future.md` — F-15 の案件名・概要・期待される効果・人間が検討すべき事項を取得する
### 1-B: プロジェクト規約の読み込み
- `CLAUDE.md` — ファイル番号体系・コーディング規約(特に「データアクセス」節)を確認する
### 1-C: 既存仕様書テンプレートの読み込み
- `docs/dev/` 配下の新機能(FP&A・レポート)系仕様書を 1 件 Read しフォーマットを把握する(例: `dev_F-01_variance_analysis.md`)
### 1-D: 関連 GAS コードの調査(Read 必須・各ファイルで確認ポイントを明記)
1. **`000_infra/002_constants.js`**:
- `SHEET_DEFAULTS` 内 `pattern: '70_bud_resource'` エントリを Read し、定義されている**実際の列名**(既知: `稼働率(%)`, `開始年月`)を記録する。`終了年月` 列の有無は SHEET_DEFAULTS に記載がないため、**1-D-5 の DDL 定義で必ず確認する(未確認のまま仕様書に書かない)**。
- `ID_PREFIX_MAP` の `70_bud_resource` エントリ(既知: `prefix: 'RSC_'`, `isDate: false`)を確認する。
- `Constants.getParam(key, defaultVal)` の仕様を確認する — `03_sys_params` シートからパラメータを読み込むヘルパー。`HEATMAP_PAST_MONTHS` 等の取得に使用する。
2. **`000_infra/003_contracts.js`**:
- `BudgetDTO` の定義形式(`@typedef` フィールド列挙スタイル)と `Contracts.toDto` / `toRow` ファクトリの使い方を Read し、`ResourceDTO` 追加の雛形を把握する。
3. **`000_infra/004_utils.js`**:
- `Utils.parseDateToYm(val)` — 対応フォーマット(Date 型・"YYYY-MM"・"YYYY/MM"・"YYYY年MM月")と戻り値(パース不可 → `""`)を確認する。
- `Utils.addMonths(ymStr, months)` — "YYYY-MM" に月数を加減算する仕様を確認する。
- `Utils.parseAmt(val)` — 数値変換の仕様(非数値 → 0)を確認する。
4. **`200_data/202_repository.js`**:
- `readSheetAsDtos_`, `appendDtosToSheet_` の内部ヘルパー仕様を Read する。
- `OrderRepository` の全コードを Read し、`_getSheet()` での `Utils.getSheetByKey(sysKey, fallbackName)` の呼び出し形式を把握する。`ResourceRepository` を同一パターンで設計するため、**sysKey 文字列は次の 1-D-5 で確認する**。
5. **`100_config/101_sys_config.js`**(最重要・必ず Read する):
- `Utils.getSheetByKey()` に渡すシステムキーの登録箇所を Read し、`70_bud_resource` に対応するシステムキー文字列(`'BUD_RSC'` または類似)を確認する。**未確認のまま仕様書に書かない**。
- `setupAllSchemas()` の実装を Read し、既存シートスキーマの DDL 登録形式(列定義・書式・バリデーション)を把握する。`69_rsc_heatmap` を DDL 管理シートとして追加するか、`buildResourceHeatmap()` が毎回再作成する動的生成シートとするかを設計判断するための情報を得る(参考: `93_kpi_dashboard` は DDL 非管理・動的生成として CLAUDE.md に明記)。
- `onOpen()` の `ui.createMenu` を Read し、**実在するメニュー名と階層構造**を把握する(造語禁止・failure_patterns.md #20 の直接対策)。
6. **`600_report/` 配下の代表的なファイル**(例: `600_report/601_datamart_ingest.js`):
- 既存データマートモジュールの関数命名規則・出力シートへの直接書き込みパターン・`ConditionalFormatRuleBuilder` の使用例(あれば)を Read し、`610_datamart_resource.js` の設計に反映する。
### 1-E: 実データ検証(MCP で実態確認)
- **`70_bud_resource` シートの実際のヘッダー列名**を MCP で取得し、DDL 定義・`SHEET_DEFAULTS` と照合する。
- 必須確認項目: メンバー名列(列名を正確に特定)・`開始年月`・`終了年月`(存在するか)・`稼働率(%)`・ID 列名(RSC_NNNN 形式)・有効フラグ列(存在するか)
- この結果を元に `ResourceDTO` の `@typedef` フィールド名を確定する。
- **`03_sys_params` シートの既存キー一覧**を MCP で取得し、`HEATMAP_*` キーの重複がないことを確認する。
---
## Phase 2: 仕様書の分割作成
出力先: `docs/dev/dev_F-15_resource_heatmap.md`
**絶対に 1 回のツール呼び出しで全内容を出力しない。以下の Step に分割して実行すること。**
### Step 2-1: 骨格の作成(File Write, ~20行)
(見出しのみ Write)
### Step 2-2: 前半セクションの追記(概要〜注意事項、~300行)
### Step 2-3a: エッジケース〜人間検討事項の追記(~200行)
### Step 2-3b: 実装プロンプト〜変更履歴の追記(~250行)
### Step 2-4: 仕様書作成プロンプトの記録(`<details>` ブロックに追記)
---
## Phase 3: 保存・登録・コミット
### 3-A: `docs/_config.json` へのナビゲーション登録(必須)
`docs/_config.json` の §E.5(FP&A・レポーティング)セクションに追記する。
### 3-B: `docs/_internal/changelog.md` への追記
### 3-C: コミット&プッシュ
(プロンプト本文は要約版。完全版は tasks/prompts/task_F-15.md.done を参照)
📌 取り込み時の注記 (2026-06-02 sub 復元)
本仕様書は旧 F-番号体系で作成され PR 未マージのまま孤立していたドラフトを、
origin/docs/dev-*ブランチから内容無改変で復元し、案件ID のみ MAS 体系へ正規化したもの。status: Open(未実装)。⚠️ ファイル番号ドリフト: 本文「対象ファイル」が指す
600_report/610〜612_*.jsは現行 main で 別機能に使用済み(610=投資分析/MAS-013・611=財務モデリング/MAS-010・612=採用sim/MAS-012)。 実装時にファイル番号の再割当が必要。ℹ️ 別ドラフト存在: 本仕様には実装アプローチの異なる別版(予算シート
BUD_RSCE駆動 + ステップ分割実装プロンプト型・638行)がorigin/docs/F-15-resource-heatmap-specブランチに保全されている。 本ファイルは概要記述のより詳細な 754行版を採用。実装時に両版を突合すること。