/**
* AIエージェント運用管理|Google Apps Script 自動生成版
* --------------------------------------------------------
* 使い方
* 1. Googleスプレッドシートを開く
* 2. 拡張機能 > Apps Script
* 3. このコードを貼り付けて保存
* 4. setupAIManagementSystem() を1回実行
* または、シート再読込後に「AI運用管理」メニューから実行
*
* standalone Apps Script から新規ファイルを作る場合:
* createNewAIManagementSpreadsheet()
*
* 注意:
* - 同名シートが既にある場合、初期構築時に削除して再作成します。
* - 本テンプレートはAPI呼び出しを前提としません。Codexが管理表を読み書きする運用です。
*/
const AI_MGMT = {
title: 'AIエージェント運用管理',
locale: 'ja_JP',
timeZone: 'Asia/Tokyo',
sheets: ['ダッシュボード', '案件管理', 'AI社員', 'タスク', '活動ログ', '成果物', '運用マニュアル', '設定'],
colors: {
text: '#3F3D54',
subText: '#726D84',
purple: '#EDE8F9',
purpleDark: '#66598C',
yellow: '#FFF2B7',
green: '#D1EFD6',
greenDeep: '#B5D6BC',
blue: '#C6EAF7',
pink: '#FFDBE8',
mauve: '#F4E8F2',
peach: '#FFD6B7',
lightYellow: '#FFF2BC',
navy: '#3A446B',
grayPurple: '#C6C1D3',
white: '#FFFFFF',
grid: '#E4E1EA',
input: '#FFF9DB',
red: '#F4CCCC'
}
};
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('AI運用管理')
.addItem('初期構築(サンプルあり)', 'setupAIManagementSystem')
.addItem('初期構築(空テンプレート)', 'setupAIManagementSystemBlank')
.addSeparator()
.addItem('サンプルデータを削除', 'clearSampleData')
.addItem('ダッシュボードを再計算', 'refreshDashboard')
.addItem('運用マニュアルを開く', 'openOperationManual')
.addToUi();
}
/** 現在のスプレッドシートにサンプル付きで構築 */
function setupAIManagementSystem() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
if (!ss) throw new Error('アクティブなGoogleスプレッドシートが見つかりません。');
buildAIManagementSystem_(ss, true);
SpreadsheetApp.getUi().alert('AIエージェント運用管理を構築しました。');
}
/** 現在のスプレッドシートに空テンプレートとして構築 */
function setupAIManagementSystemBlank() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
if (!ss) throw new Error('アクティブなGoogleスプレッドシートが見つかりません。');
buildAIManagementSystem_(ss, false);
SpreadsheetApp.getUi().alert('空テンプレートを構築しました。');
}
/** standalone Apps Script から新規作成する場合 */
function createNewAIManagementSpreadsheet() {
const ss = SpreadsheetApp.create(AI_MGMT.title);
buildAIManagementSystem_(ss, true);
Logger.log('作成完了: ' + ss.getUrl());
return ss.getUrl();
}
function buildAIManagementSystem_(ss, includeSampleData) {
const c = AI_MGMT.colors;
ss.setSpreadsheetLocale(AI_MGMT.locale);
ss.setSpreadsheetTimeZone(AI_MGMT.timeZone);
// 同名シートを再構築。最低1枚は残す必要があるため、一時シートを使う。
const tempName = '__BUILD_TEMP__';
let temp = ss.getSheetByName(tempName);
if (!temp) temp = ss.insertSheet(tempName);
AI_MGMT.sheets.forEach(name => {
const sh = ss.getSheetByName(name);
if (sh) ss.deleteSheet(sh);
});
const dashboard = ss.insertSheet('ダッシュボード');
const projects = ss.insertSheet('案件管理');
const agents = ss.insertSheet('AI社員');
const tasks = ss.insertSheet('タスク');
const logs = ss.insertSheet('活動ログ');
const outputs = ss.insertSheet('成果物');
const manual = ss.insertSheet('運用マニュアル');
const settings = ss.insertSheet('設定');
buildProjects_(projects, includeSampleData);
buildAgents_(agents, includeSampleData);
buildTasks_(tasks, includeSampleData);
buildLogs_(logs, includeSampleData);
buildOutputs_(outputs, includeSampleData);
buildOperationManual_(manual);
buildSettings_(settings);
buildDashboard_(dashboard);
// タブ色
dashboard.setTabColor(c.purpleDark);
projects.setTabColor(c.yellow);
agents.setTabColor(c.greenDeep);
tasks.setTabColor(c.blue);
logs.setTabColor(c.grayPurple);
outputs.setTabColor(c.pink);
manual.setTabColor(c.purpleDark);
settings.setTabColor(c.navy);
// 先頭をダッシュボードに
ss.setActiveSheet(dashboard);
ss.moveActiveSheet(1);
if (ss.getSheets().length > 1 && ss.getSheetByName(tempName)) {
ss.deleteSheet(temp);
}
SpreadsheetApp.flush();
}
function buildProjects_(sh, sample) {
const c = AI_MGMT.colors;
prepareDataSheet_(sh, 'AIエージェント案件管理', '案件の進捗と担当AIを確認します。入力欄は淡い黄色です。', 14);
const headers = ['案件ID','案件名','依頼日','優先度','ステータス','現在工程','担当AI','進捗率','最終更新','備考','プロジェクトゴール','運用指標','値','補足'];
sh.getRange(4,1,1,headers.length).setValues([headers]);
styleHeader_(sh.getRange(4,1,1,11));
sh.getRange('L4:N4').setBackground(c.purpleDark).setFontColor(c.white).setFontWeight('bold').setHorizontalAlignment('center');
if (sample) {
sh.getRange('A5:K6').setValues([
['P-0001','玉川村アウトドア観光サイト',new Date(2026,8,7),'高','進行中','UX設計','レン',0.35,new Date(2026,8,7,10,30),'サンプル案件','自然豊かな玉川村の魅力を伝え、観光客にイベント参加と来訪を促すランディングページを完成させる'],
['P-0002','新規案件をここに入力','', '中','未着手','要件整理','ミカ',0,'','','']
]);
}
// KPI
sh.getRange('L5:N10').setValues([
['進行中','','ステータスから集計'],
['完了','','ステータスから集計'],
['平均進捗','','案件の平均'],
['総案件数','','IDの件数'],
['今日のログ','','活動ログから集計'],
['稼働AI','','AI社員から集計']
]);
sh.getRange('M5').setFormula('=COUNTIF(E5:E250,"進行中")');
sh.getRange('M6').setFormula('=COUNTIF(E5:E250,"完了")');
sh.getRange('M7').setFormula('=IFERROR(AVERAGE(H5:H250),"")');
sh.getRange('M8').setFormula('=COUNTA(A5:A250)');
sh.getRange('M9').setFormula('=COUNTIF(\'活動ログ\'!B5:B600,">="&TODAY())');
sh.getRange('M10').setFormula('=COUNTIF(\'AI社員\'!D5:D50,"稼働中")');
sh.getRange('L5:N10').setBorder(true,true,true,true,true,true,c.grid,SpreadsheetApp.BorderStyle.SOLID);
sh.getRange('L5:L10').setFontWeight('bold');
// 入力エリア
sh.getRange('A5:K250').setBackground(c.input);
if (sample) sh.getRange('A5:K6').setBackground(c.lightYellow);
sh.getRange('L5:N10').setBackground(c.white);
sh.getRange('H5:H250').setNumberFormat('0%');
sh.getRange('C5:C250').setNumberFormat('yyyy-mm-dd');
sh.getRange('I5:I250').setNumberFormat('yyyy-mm-dd hh:mm');
sh.getRange('K5:K250').setWrap(true);
setListValidation_(sh.getRange('D5:D250'), ['緊急','高','中','低']);
setListValidation_(sh.getRange('E5:E250'), ['未着手','進行中','承認待ち','完了','保留','エラー']);
setListValidation_(sh.getRange('F5:F250'), ['要件整理','UX設計','デザイン','実装','品質検査','納品']);
setNumberValidation_(sh.getRange('H5:H250'), 0, 1);
// ステータスの条件付き書式
addTextRules_(sh, 'E5:E250', {
'進行中': c.blue,
'完了': c.green,
'承認待ち': c.yellow,
'エラー': c.red,
'保留': c.grayPurple
});
sh.setFrozenRows(4);
setWidths_(sh, [90,230,110,110,110,110,110,100,125,120,360,110,100,160]);
sh.setRowHeight(4,30);
}
function buildAgents_(sh, sample) {
const c = AI_MGMT.colors;
prepareDataSheet_(sh, 'AI社員名簿', '役割、稼働状況、利用モデルを管理します。', 8);
const headers = ['AI ID','名前','役割','状態','担当案件','最終稼働','完了タスク','モデル'];
sh.getRange(4,1,1,8).setValues([headers]);
styleHeader_(sh.getRange(4,1,1,8));
if (sample) {
sh.getRange('A5:H9').setValues([
['A-01','ミカ','ディレクター','稼働中','P-0001',new Date(2026,8,7,10,30),12,'gpt-5.6-sol'],
['A-02','レン','UXプランナー','稼働中','P-0001',new Date(2026,8,7,10,31),9,'gpt-5.6-terra'],
['A-03','アオイ','UIデザイナー','待機中','',new Date(2026,8,7,9,45),15,'gpt-5.6-terra'],
['A-04','カイ','エンジニア','待機中','',new Date(2026,8,7,9,20),18,'gpt-5.6-sol'],
['A-05','ユイ','品質管理・QA','待機中','',new Date(2026,8,7,9,10),21,'gpt-5.6-luna']
]);
}
sh.getRange('A5:H50').setBackground(c.input);
if (sample) sh.getRange('A5:H9').setBackground(c.white);
sh.getRange('F5:F50').setNumberFormat('yyyy-mm-dd hh:mm');
setListValidation_(sh.getRange('D5:D50'), ['待機中','稼働中','確認待ち','停止中','エラー']);
addTextRules_(sh, 'D5:D50', {
'稼働中': c.green,
'待機中': c.grayPurple,
'確認待ち': c.yellow,
'停止中': c.peach,
'エラー': c.red
});
sh.setFrozenRows(4);
setWidths_(sh, [115,115,145,115,115,145,115,145]);
sh.setRowHeight(4,30);
}
function buildTasks_(sh, sample) {
const c = AI_MGMT.colors;
prepareDataSheet_(sh, 'タスクキュー', 'エージェント間の引き継ぎと実行結果を追跡します。', 11);
const headers = ['タスクID','案件ID','タスク名','担当AI','ステータス','優先度','作成日時','開始日時','完了日時','成果物・結果','再試行回数'];
sh.getRange(4,1,1,11).setValues([headers]);
styleHeader_(sh.getRange(4,1,1,11));
if (sample) {
sh.getRange('A5:K7').setValues([
['T-0001','P-0001','依頼内容を要件へ分解','ミカ','完了','高',new Date(2026,8,7,10,0),new Date(2026,8,7,10,2),new Date(2026,8,7,10,12),'要件定義を作成',0],
['T-0002','P-0001','ページ構成と導線を設計','レン','実行中','高',new Date(2026,8,7,10,12),new Date(2026,8,7,10,15),'','',0],
['T-0003','P-0001','デザイン仕様を作成','アオイ','待機','中',new Date(2026,8,7,10,12),'','','',0]
]);
}
sh.getRange('A5:K350').setBackground(c.input);
if (sample) sh.getRange('A5:K7').setBackground(c.white);
sh.getRange('G5:I350').setNumberFormat('yyyy-mm-dd hh:mm');
setListValidation_(sh.getRange('E5:E350'), ['待機','実行中','確認待ち','完了','失敗','取消']);
setListValidation_(sh.getRange('F5:F350'), ['緊急','高','中','低']);
addTextRules_(sh, 'E5:E350', {
'実行中': c.blue,
'完了': c.green,
'確認待ち': c.yellow,
'失敗': c.red,
'取消': c.grayPurple
});
sh.setFrozenRows(4);
setWidths_(sh, [120,120,230,120,120,120,140,140,140,230,120]);
sh.setRowHeight(4,30);
}
function buildLogs_(sh, sample) {
const c = AI_MGMT.colors;
prepareDataSheet_(sh, '活動ログ', 'Codexが作業の開始・進捗・引き継ぎ・完了・エラーを追記する監査ログです。', 8);
const headers = ['ログID','日時','案件ID','タスクID','AI社員','イベント','概要','処理秒数'];
sh.getRange(4,1,1,8).setValues([headers]);
styleHeader_(sh.getRange(4,1,1,8));
if (sample) {
sh.getRange('A5:H8').setValues([
['L-0001',new Date(2026,8,7,10,0,0),'P-0001','T-0001','ミカ','開始','案件を受領しました',2],
['L-0002',new Date(2026,8,7,10,12,0),'P-0001','T-0001','ミカ','完了','要件定義を作成しました',600],
['L-0003',new Date(2026,8,7,10,13,0),'P-0001','T-0002','ミカ','引き継ぎ','レンへUX設計を依頼しました',3],
['L-0004',new Date(2026,8,7,10,15,0),'P-0001','T-0002','レン','開始','サイト構成の作成を開始しました',1]
]);
}
sh.getRange('B5:B600').setNumberFormat('yyyy-mm-dd hh:mm:ss');
setListValidation_(sh.getRange('F5:F600'), ['受付','開始','ツール実行','引き継ぎ','完了','警告','エラー','再試行']);
addTextRules_(sh, 'F5:F600', {
'開始': c.blue,
'完了': c.green,
'引き継ぎ': c.mauve,
'警告': c.yellow,
'エラー': c.red,
'再試行': c.peach
});
sh.setFrozenRows(4);
setWidths_(sh, [115,145,115,115,115,115,300,115]);
sh.setRowHeight(4,30);
}
function buildOutputs_(sh, sample) {
const c = AI_MGMT.colors;
prepareDataSheet_(sh, '成果物管理', '生成物の保存先、品質、承認状況を管理します。', 9);
const headers = ['成果物ID','案件ID','種類','名称','Google Driveリンク','QAスコア','承認','状態','更新日時'];
sh.getRange(4,1,1,9).setValues([headers]);
styleHeader_(sh.getRange(4,1,1,9));
if (sample) {
sh.getRange('A5:I6').setValues([
['D-0001','P-0001','要件定義','要件定義書','',94,'未確認','作成済み',new Date(2026,8,7,10,12)],
['D-0002','P-0001','Webサイト','ランディングページ','', '', '未確認','作成中',new Date(2026,8,7,10,30)]
]);
}
sh.getRange('A5:I250').setBackground(c.input);
if (sample) sh.getRange('A5:I6').setBackground(c.white);
sh.getRange('I5:I250').setNumberFormat('yyyy-mm-dd hh:mm');
setListValidation_(sh.getRange('C5:C250'), ['要件定義','構成案','デザイン','Webサイト','QAレポート','その他']);
setListValidation_(sh.getRange('G5:G250'), ['未確認','承認','差し戻し']);
setListValidation_(sh.getRange('H5:H250'), ['作成中','作成済み','納品済み','エラー']);
setNumberValidation_(sh.getRange('F5:F250'), 0, 100);
addTextRules_(sh, 'H5:H250', {
'作成中': c.blue,
'作成済み': c.green,
'納品済み': c.greenDeep,
'エラー': c.red
});
sh.setFrozenRows(4);
setWidths_(sh, [115,115,115,210,260,115,115,115,145]);
sh.setRowHeight(4,30);
}
function buildSettings_(sh) {
const c = AI_MGMT.colors;
prepareDataSheet_(sh, 'システム設定', 'Codex運用時の共通ルールです。APIキーやモデル呼び出し設定は使用しません。', 3);
sh.getRange('A4:C4').setValues([['設定キー','値','説明']]);
styleHeader_(sh.getRange('A4:C4'));
sh.getRange('A5:C10').setValues([
['OPERATION_MODE','CODEX','Codexがシートを読み書きする運用'],
['REQUIRE_TASK_ID',true,'活動ログは原則としてタスクIDと紐づける'],
['REQUIRE_PROJECT_ID',true,'活動ログは原則として案件IDと紐づける'],
['REQUIRE_APPROVAL',true,'重要成果物・納品前は人間確認を挟む'],
['LOG_ON_STATUS_CHANGE',true,'開始・完了・失敗・引継ぎ時は必ずログを残す'],
['LOG_RETENTION_DAYS',90,'活動ログの標準保持日数']
]);
sh.getRange('B5:B10').setBackground(c.input);
sh.getRange('A5:A10').setFontWeight('bold');
sh.setFrozenRows(4);
setWidths_(sh, [210,180,380]);
sh.setRowHeight(4,30);
}
function buildOperationManual_(sh) {
const c = AI_MGMT.colors;
sh.clear();
sh.setHiddenGridlines(true);
sh.getRange('A1:H80').setFontFamily('Arial').setFontColor(c.text).setVerticalAlignment('top');
sh.setColumnWidth(1, 42);
sh.setColumnWidth(2, 190);
sh.setColumnWidth(3, 260);
sh.setColumnWidth(4, 250);
sh.setColumnWidth(5, 180);
sh.setColumnWidth(6, 180);
sh.setColumnWidth(7, 180);
sh.setColumnWidth(8, 180);
mergeAndSet_(sh, 'A1:H2', 'Codex運用マニュアル|AIエージェント運用管理');
sh.getRange('A1:H2').setBackground(c.purple).setFontSize(18).setFontWeight('bold').setVerticalAlignment('middle');
mergeAndSet_(sh, 'A3:H3', 'このスプレッドシートは、APIでAIを呼び出す仕組みではなく、Codexが案件・タスクを確認し、作業結果を各管理タブへ直接記録する運用を前提とします。');
sh.getRange('A3:H3').setFontSize(10).setFontColor(c.subText).setWrap(true);
section_(sh, 5, '1. 基本原則');
manualRows_(sh, 6, [
['①','案件から開始','Codexは作業開始前に「案件管理」で案件ID、目的、現在工程、プロジェクトゴールを確認する。'],
['②','タスク単位で実行','実作業は「タスク」のタスクIDを単位として扱い、開始時にステータスを「実行中」へ更新する。'],
['③','ログを必ず残す','開始・重要な判断・引き継ぎ・完了・警告・エラー・再試行のタイミングで「活動ログ」へ1行追記する。'],
['④','成果物を登録','ファイル、文書、コード、調査結果など再利用可能な成果ができたら「成果物」へ登録する。'],
['⑤','勝手に完了扱いにしない','人間確認が必要なタスクは「確認待ち」とし、承認前に「完了」「納品済み」へ変更しない。']
]);
section_(sh, 13, '2. Codexの標準実行フロー');
sh.getRange('A14:H14').setValues([['STEP','操作','確認するタブ','更新するタブ','必須ログ','完了条件','','']]);
sh.getRange('A14:F14').setBackground(c.navy).setFontColor(c.white).setFontWeight('bold').setHorizontalAlignment('center');
sh.getRange('A15:F21').setValues([
['1','対象案件を特定','案件管理','-','受付','案件ID・ゴール・優先度を把握'],
['2','対象タスクを特定','タスク','タスク','開始','ステータス=実行中、開始日時を記録'],
['3','作業を実行','案件管理/タスク','必要に応じ各タブ','ツール実行','作業途中の重要判断を記録'],
['4','成果を保存','-','成果物','ツール実行','成果物ID・名称・リンク等を登録'],
['5','品質確認','成果物/タスク','成果物/タスク','警告 or 完了','問題なければ完了、要確認なら確認待ち'],
['6','次担当へ渡す','タスク/AI社員','タスク/AI社員','引き継ぎ','次担当・次タスクが明確'],
['7','終了処理','タスク','タスク/案件管理','完了','完了日時・結果・進捗を更新']
]);
sh.getRange('A14:F21').setBorder(true,true,true,true,true,true,c.grid,SpreadsheetApp.BorderStyle.SOLID).setWrap(true);
section_(sh, 23, '3. 「活動ログ」への書き込みルール');
sh.getRange('A24:H24').setValues([['列','項目','Codexが書く内容','例','','','','']]);
sh.getRange('A24:D24').setBackground(c.navy).setFontColor(c.white).setFontWeight('bold').setHorizontalAlignment('center');
sh.getRange('A25:D32').setValues([
['A','ログID','既存末尾+1。L-0001形式で重複させない','L-0042'],
['B','日時','書き込んだ時点の日本時間','2026-09-09 11:15:20'],
['C','案件ID','案件管理の案件ID','P-0001'],
['D','タスクID','タスク表のタスクID','T-0007'],
['E','AI社員','作業主体。Codex運用では担当AI名またはCodex','カイ / Codex'],
['F','イベント','受付・開始・ツール実行・引き継ぎ・完了・警告・エラー・再試行','完了'],
['G','概要','何をしたか、結果、次に必要なことを1~3文で具体化','資料構成を修正。第3章の重複を削除し成果物D-0005を更新。'],
['H','処理秒数','分かる場合のみ。分からなければ空欄でもよい','85']
]);
sh.getRange('A24:D32').setBorder(true,true,true,true,true,true,c.grid,SpreadsheetApp.BorderStyle.SOLID).setWrap(true);
section_(sh, 34, '4. Codexに渡す共通指示文');
mergeAndSet_(sh, 'A35:H42',
'あなたはこのGoogleスプレッドシートを作業台帳として使用してください。作業開始前に「案件管理」と「タスク」を確認し、対象の案件ID・タスクID・プロジェクトゴールを把握してください。作業開始時は対象タスクを「実行中」に変更し開始日時を記録してください。作業中は重要な判断、ツール実行、引き継ぎ、警告、エラー、再試行を「活動ログ」に追記してください。作業完了時は「タスク」のステータス、完了日時、成果物・結果を更新し、再利用可能な成果物がある場合は「成果物」に登録してください。人間の確認が必要な事項は勝手に完了扱いにせず「確認待ち」としてください。既存データを削除・上書きする場合は必要性を確認し、履歴が追える状態を保ってください。'
);
sh.getRange('A35:H42').setBackground(c.lightYellow).setWrap(true).setFontSize(10).setBorder(true,true,true,true,false,false,c.grid,SpreadsheetApp.BorderStyle.SOLID);
section_(sh, 44, '5. ステータス更新の基準');
sh.getRange('A45:D45').setValues([['対象','変更前','変更後','条件']]);
sh.getRange('A45:D45').setBackground(c.navy).setFontColor(c.white).setFontWeight('bold').setHorizontalAlignment('center');
sh.getRange('A46:D52').setValues([
['タスク','待機','実行中','Codexが実作業を開始した時'],
['タスク','実行中','確認待ち','人間の判断・承認が必要な時'],
['タスク','実行中','完了','要求された成果が揃い、追加確認が不要な時'],
['タスク','実行中','失敗','解消できないエラーで処理継続不能な時'],
['AI社員','待機中','稼働中','担当タスクの実行開始時'],
['AI社員','稼働中','確認待ち','人間確認待ちになった時'],
['AI社員','稼働中','待機中','担当タスク終了後、次タスクがない時']
]);
sh.getRange('A45:D52').setBorder(true,true,true,true,true,true,c.grid,SpreadsheetApp.BorderStyle.SOLID).setWrap(true);
section_(sh, 54, '6. 禁止事項');
manualRows_(sh, 55, [
['×','ログを残さず更新','案件・タスクの状態だけ変更し、活動ログを残さない運用は禁止。'],
['×','IDの作り直し','既存の案件ID・タスクID・成果物ID・ログIDを別用途に再利用しない。'],
['×','過去ログの書換え','誤記訂正を除き、過去の活動ログは原則上書きしない。訂正も新規ログで理由を残す。'],
['×','承認の代行','人間の承認が設定されている成果物をCodex判断だけで承認済みにしない。'],
['×','機密情報の記録','パスワード、APIキー、秘密鍵、個人の認証情報をセルや活動ログへ書かない。']
]);
section_(sh, 62, '7. 運用開始時チェック');
sh.getRange('A63:C63').setValues([['確認','チェック項目','基準']]);
sh.getRange('A63:C63').setBackground(c.navy).setFontColor(c.white).setFontWeight('bold').setHorizontalAlignment('center');
sh.getRange('A64:C69').setValues([
['□','案件IDがある','案件管理に対象案件が登録済み'],
['□','プロジェクトゴールがある','Codexが目的を判断できる具体度'],
['□','タスクIDがある','実作業単位にタスクが分解されている'],
['□','担当AIが決まっている','AI社員名またはCodexを明記'],
['□','成果物の置き場所が決まっている','Drive、Docs、コード等の保存先が明確'],
['□','承認要否が明確','人間確認が必要なポイントを事前に把握']
]);
sh.getRange('A63:C69').setBorder(true,true,true,true,true,true,c.grid,SpreadsheetApp.BorderStyle.SOLID).setWrap(true);
sh.setFrozenRows(3);
for (let r=1; r<=70; r++) sh.setRowHeight(r, 24);
sh.setRowHeight(1, 34);
sh.setRowHeight(2, 34);
for (let r=35; r<=42; r++) sh.setRowHeight(r, 32);
}
function section_(sh, row, title) {
const c = AI_MGMT.colors;
sh.getRange(row,1,1,8).merge().setValue(title).setBackground(c.purpleDark).setFontColor(c.white).setFontWeight('bold').setFontSize(11).setVerticalAlignment('middle');
}
function manualRows_(sh, startRow, rows) {
const c = AI_MGMT.colors;
const data = rows.map(r => [r[0], r[1], r[2], '', '', '', '', '']);
sh.getRange(startRow,1,data.length,8).setValues(data).setWrap(true).setBorder(true,true,true,true,true,true,c.grid,SpreadsheetApp.BorderStyle.SOLID);
sh.getRange(startRow,1,data.length,1).setHorizontalAlignment('center').setFontWeight('bold');
sh.getRange(startRow,2,data.length,1).setFontWeight('bold');
}
function buildDashboard_(sh) {
const c = AI_MGMT.colors;
sh.clear();
sh.setHiddenGridlines(true);
sh.setFrozenRows(3);
sh.getRange('A1:V36').setFontFamily('Arial').setFontColor(c.text).setVerticalAlignment('middle');
for (let col = 1; col <= 22; col++) sh.setColumnWidth(col, 44);
// タイトル
mergeAndSet_(sh, 'A1:V2', 'ピコピコ制作所|AIエージェント稼働ダッシュボード');
sh.getRange('A1:V2').setBackground(c.purple).setFontSize(18).setFontWeight('bold').setHorizontalAlignment('left');
mergeAndSet_(sh, 'A3:V3', 'AI社員の状態・作業・タスクをリアルタイム表示');
sh.getRange('A3:V3').setFontSize(9).setFontColor(c.subText);
// KPI cards
dashboardKpi_(sh, 'A5:D5','A6:D8','進行中の案件','=COUNTIF(\'案件管理\'!E5:E250,"進行中")',c.yellow);
dashboardKpi_(sh, 'F5:I5','F6:I8','活動中のAI社員','=COUNTIF(\'AI社員\'!D5:D50,"稼働中")',c.green);
dashboardKpi_(sh, 'K5:N5','K6:N8','完了タスク','=COUNTIF(\'タスク\'!E5:E350,"完了")',c.blue);
dashboardKpi_(sh, 'P5:S5','P6:S8','エラー','=COUNTIF(\'タスク\'!E5:E350,"失敗")+COUNTIF(\'AI社員\'!D5:D50,"エラー")',c.pink);
mergeAndSet_(sh, 'A9:V10', '🏢 AI制作フロア ● 表示あり=活動中 空席=待機・完了');
sh.getRange('A9:V10').setBackground(c.mauve).setFontSize(11).setFontWeight('bold');
sh.getRange('A11').setFormula('=IFERROR("🎯 現在のプロジェクトゴール|"&INDEX(FILTER(\'案件管理\'!K5:K250,\'案件管理\'!E5:E250="進行中"),1),"進行中のプロジェクトはありません")');
sh.getRange('A11:V12').merge().setBackground(c.yellow).setFontSize(12).setFontWeight('bold').setWrap(true);
// AI社員カード 5名
agentCard_(sh, 5, 'B13:F14','B15:F17','B18:F18','C19:E20','👩💼', c.peach);
agentCard_(sh, 6, 'I13:M14','I15:M17','I18:M18','J19:L20','🧑🏫', c.blue);
agentCard_(sh, 7, 'P13:T14','P15:T17','P18:T18','Q19:S20','👩🎨', c.purple);
agentCard_(sh, 8, 'F24:J25','F26:J28','F29:J29','G30:I31','👨💻', c.green);
agentCard_(sh, 9, 'M24:Q25','M26:Q28','M29:Q29','N30:P31','👩🔬', c.pink);
// 最新ログ
sh.getRange('A35').setFormula('="最新ログ "&IFERROR(INDEX(\'活動ログ\'!B5:B600,COUNTA(\'活動ログ\'!A5:A600))&" "&INDEX(\'活動ログ\'!E5:E600,COUNTA(\'活動ログ\'!A5:A600))&": "&INDEX(\'活動ログ\'!G5:G600,COUNTA(\'活動ログ\'!A5:A600)),"まだログはありません")');
sh.getRange('A35:V36').merge().setBackground(c.grayPurple).setFontColor(c.white).setFontSize(10).setWrap(true);
// 行高
[1,2].forEach(r => sh.setRowHeight(r,34));
sh.setRowHeight(3,24);
for (let r = 4; r <= 36; r++) sh.setRowHeight(r,24);
[11,12].forEach(r => sh.setRowHeight(r,34));
[15,16,17,26,27,28].forEach(r => sh.setRowHeight(r,31));
}
function dashboardKpi_(sh, labelRange, valueRange, label, formula, bg) {
const c = AI_MGMT.colors;
mergeAndSet_(sh, labelRange, label);
sh.getRange(labelRange).setBackground(bg).setFontWeight('bold').setFontSize(9).setHorizontalAlignment('center')
.setBorder(true,true,true,true,false,false,c.grid,SpreadsheetApp.BorderStyle.SOLID);
const topLeft = valueRange.split(':')[0];
sh.getRange(topLeft).setFormula(formula);
sh.getRange(valueRange).merge().setBackground(c.white).setFontSize(22).setFontWeight('bold').setHorizontalAlignment('center')
.setBorder(true,true,true,true,false,false,c.grid,SpreadsheetApp.BorderStyle.SOLID);
}
function agentCard_(sh, sourceRow, bubbleRange, iconRange, nameRange, stateRange, emoji, bg) {
const c = AI_MGMT.colors;
const bubbleCell = bubbleRange.split(':')[0];
const iconCell = iconRange.split(':')[0];
const nameCell = nameRange.split(':')[0];
const stateCell = stateRange.split(':')[0];
sh.getRange(bubbleCell).setFormula(
'=IF(\'AI社員\'!D' + sourceRow + '="稼働中","💬 "&IFERROR(INDEX(FILTER(\'タスク\'!C$5:C$350,\'タスク\'!D$5:D$350=\'AI社員\'!B' + sourceRow + ',\'タスク\'!E$5:E$350="実行中"),1),"作業内容を確認中"),"")'
);
sh.getRange(bubbleRange).merge().setBackground(c.white).setFontSize(9).setWrap(true)
.setBorder(true,true,true,true,false,false,c.grid,SpreadsheetApp.BorderStyle.SOLID);
sh.getRange(iconCell).setFormula('=IF(\'AI社員\'!D' + sourceRow + '="稼働中","' + emoji + '","")');
sh.getRange(iconRange).merge().setBackground(bg).setFontSize(26).setHorizontalAlignment('center')
.setBorder(true,true,true,true,false,false,c.grid,SpreadsheetApp.BorderStyle.SOLID);
sh.getRange(nameCell).setFormula('=\'AI社員\'!B' + sourceRow + '&"|"&\'AI社員\'!C' + sourceRow);
sh.getRange(nameRange).merge().setBackground(c.grayPurple).setFontColor(c.white).setFontWeight('bold').setFontSize(9).setHorizontalAlignment('center');
sh.getRange(stateCell).setFormula('=IF(\'AI社員\'!D' + sourceRow + '="稼働中","● 活動中","▣ 空席")');
sh.getRange(stateRange).merge().setBackground(c.white).setFontWeight('bold').setFontSize(8).setHorizontalAlignment('center')
.setBorder(true,true,true,true,false,false,c.grid,SpreadsheetApp.BorderStyle.SOLID);
}
function prepareDataSheet_(sh, title, subtitle, colCount) {
const c = AI_MGMT.colors;
sh.clear();
sh.setHiddenGridlines(true);
sh.getRange(1,1,Math.max(sh.getMaxRows(), 50),colCount).setFontFamily('Arial').setFontColor(c.text).setVerticalAlignment('middle');
sh.getRange(1,1).setValue(title).setFontSize(16).setFontWeight('bold');
sh.getRange(2,1).setValue(subtitle).setFontSize(9).setFontColor(c.subText);
sh.getRange(1,1,1,colCount).merge();
sh.getRange(2,1,1,colCount).merge();
sh.setRowHeight(1,30);
sh.setRowHeight(2,24);
}
function styleHeader_(range) {
const c = AI_MGMT.colors;
range.setBackground(c.navy).setFontColor(c.white).setFontWeight('bold').setHorizontalAlignment('center').setWrap(true);
}
function mergeAndSet_(sh, a1, value) {
sh.getRange(a1).merge();
sh.getRange(a1.split(':')[0]).setValue(value);
}
function setWidths_(sh, widths) {
widths.forEach((w, i) => sh.setColumnWidth(i + 1, w));
}
function setListValidation_(range, values) {
const rule = SpreadsheetApp.newDataValidation()
.requireValueInList(values, true)
.setAllowInvalid(false)
.build();
range.setDataValidation(rule);
}
function setNumberValidation_(range, min, max) {
const rule = SpreadsheetApp.newDataValidation()
.requireNumberBetween(min, max)
.setAllowInvalid(false)
.build();
range.setDataValidation(rule);
}
function addTextRules_(sh, a1, mapping) {
const range = sh.getRange(a1);
const rules = sh.getConditionalFormatRules();
Object.keys(mapping).forEach(text => {
rules.push(
SpreadsheetApp.newConditionalFormatRule()
.whenTextEqualTo(text)
.setBackground(mapping[text])
.setRanges([range])
.build()
);
});
sh.setConditionalFormatRules(rules);
}
/**
* 活動ログへ1件追記する共通関数
* 使用例:
* writeActivityLog('P-0001', 'T-0002', 'レン', '開始', 'UX設計を開始しました', 1.2);
*/
function writeActivityLog(projectId, taskId, agentName, eventName, summary, processingSec) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sh = ss.getSheetByName('活動ログ');
if (!sh) throw new Error('「活動ログ」シートがありません。先に初期構築を実行してください。');
const nextRow = Math.max(5, sh.getLastRow() + 1);
const logId = 'L-' + String(nextRow - 4).padStart(4, '0');
sh.getRange(nextRow, 1, 1, 8).setValues([[
logId,
new Date(),
projectId || '',
taskId || '',
agentName || '',
eventName || '',
summary || '',
Number(processingSec || 0)
]]);
sh.getRange(nextRow, 2).setNumberFormat('yyyy-mm-dd hh:mm:ss');
return logId;
}
/** 新規案件を1件追加 */
function addProject(projectName, priority, goal) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sh = ss.getSheetByName('案件管理');
if (!sh) throw new Error('「案件管理」シートがありません。');
const row = nextEmptyRow_(sh, 1, 5, 250);
const id = 'P-' + String(row - 4).padStart(4, '0');
sh.getRange(row,1,1,11).setValues([[
id, projectName || '', new Date(), priority || '中', '未着手', '要件整理', '', 0, new Date(), '', goal || ''
]]);
sh.getRange(row,3).setNumberFormat('yyyy-mm-dd');
sh.getRange(row,8).setNumberFormat('0%');
sh.getRange(row,9).setNumberFormat('yyyy-mm-dd hh:mm');
return id;
}
/** 新規タスクを1件追加 */
function addTask(projectId, taskName, agentName, priority) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sh = ss.getSheetByName('タスク');
if (!sh) throw new Error('「タスク」シートがありません。');
const row = nextEmptyRow_(sh, 1, 5, 350);
const id = 'T-' + String(row - 4).padStart(4, '0');
sh.getRange(row,1,1,11).setValues([[
id, projectId || '', taskName || '', agentName || '', '待機', priority || '中', new Date(), '', '', '', 0
]]);
sh.getRange(row,7,1,3).setNumberFormat('yyyy-mm-dd hh:mm');
return id;
}
/** 新規成果物を1件追加 */
function addDeliverable(projectId, type, name, driveUrl) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sh = ss.getSheetByName('成果物');
if (!sh) throw new Error('「成果物」シートがありません。');
const row = nextEmptyRow_(sh, 1, 5, 250);
const id = 'D-' + String(row - 4).padStart(4, '0');
sh.getRange(row,1,1,9).setValues([[
id, projectId || '', type || 'その他', name || '', driveUrl || '', '', '未確認', '作成中', new Date()
]]);
sh.getRange(row,9).setNumberFormat('yyyy-mm-dd hh:mm');
return id;
}
function nextEmptyRow_(sh, keyCol, startRow, maxRow) {
const vals = sh.getRange(startRow, keyCol, maxRow - startRow + 1, 1).getValues();
for (let i = 0; i < vals.length; i++) {
if (!vals[i][0]) return startRow + i;
}
throw new Error('入力可能な行が上限に達しました。');
}
/** サンプル行のみ削除し、運用用の空テンプレートにする */
function clearSampleData() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const targets = [
['案件管理','A5:K250'],
['AI社員','A5:H50'],
['タスク','A5:K350'],
['活動ログ','A5:H600'],
['成果物','A5:I250']
];
targets.forEach(([name, a1]) => {
const sh = ss.getSheetByName(name);
if (sh) sh.getRange(a1).clearContent();
});
SpreadsheetApp.flush();
}
function refreshDashboard() {
SpreadsheetApp.flush();
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sh = ss.getSheetByName('ダッシュボード');
if (sh) ss.setActiveSheet(sh);
}
function openOperationManual() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sh = ss.getSheetByName('運用マニュアル');
if (!sh) throw new Error('「運用マニュアル」シートがありません。初期構築を実行してください。');
ss.setActiveSheet(sh);
}