見出し画像

【コピペでOK】Googleスプレッドシートが最強のAIプロジェクト管理ツールに変身!Gemini APIでタスク管理を自動化しよう

どーも。
鈴木優作です。

本日スプレッドシート✖️AIの活用方法についてポストをしたところ、ご好評をいただいたため、本日から1週間限定にて実装方法を公開させていただきます。(11/3まで)

「新しいプロジェクトが始まったけど、タスクの洗い出しが大変…」
「WBS(作業分解構成図)を作るのに毎回時間がかかる…」

そんな経験はありませんか?

私自身、プロジェクト計画をすることが多く、どうか効率化を測れないかと頭を抱えていた背景から、この構築に着手をいたしました。

この記事では、普段使っているGoogleスプレッドシートに、Googleの最新AIである「Gemini」を連携させる方法をご紹介します。

専門知識は一切不要!この記事にあるコードをコピペするだけで、あなたのスプレッドシートがこんな風に進化します。

  • AIとの対話でプロジェクト計画を自動作成

  • ボタン一つでタスクをシートに自動反映

  • ガントチャートや期日サマリーも全自動で更新

もう面倒なタスク洗い出しに時間をかける必要はありません。AIに任せて、あなたはもっと重要な仕事に集中しましょう!


準備するもの

必要なものはたったの2つだけです。

  1. Googleアカウント

  2. GeminiのAPIキー(無料で取得できます。後ほど詳しく解説します)


作成手順【コピペでOK!】

ここからは、実際に設定を進めていきましょう。手順通りに進めれば、誰でも必ず完成できます。

ステップ1:スプレッドシートの準備

まずは、このツールの土台となるスプレッドシートを用意します。

  1. 新しいスプレッドシートを作成し、好きな名前をつけましょう。(例:「プロジェクト管理シート」)

  2. 左下にあるシート名を「プロジェクト管理」に変更します。(シート名をダブルクリックすると編集できます)

  3. 1行目に、以下の項目をA列からJ列まで順番にコピー&ペーストしてください。

画像

A列|B列|C列|D列|E列|F列|G列|H列|I列|J列|
No.|種別|顧客名|プロジェクト・タスク名|担当者|ステータス|進捗率|開始日|終了日|最終更新日|

ステップ2:スクリプトエディタを開く

次に、コードを貼り付けるための「スクリプトエディタ」を開きます。

  1. スプレッドシートのメニューから 拡張機能 > Apps Script を選択してください。

画像

ステップ3:コードを貼り付ける

スクリプトエディタが開いたら、2種類のコードを貼り付けていきます。

1. コード.gs の設定

  • はじめに表示されている コード.gs の中身(function myFunction() { ... }など)をすべて削除します。

  • 以下のコード全文をコピーして、空になった コード.gs に貼り付けます。

▼▼▼ [コード.gs] に貼り付けるコード (全文) ▼▼▼

codeJavaScript

// =============================================
// 設定項目
// =============================================
const PROJECT_SHEET_NAME = 'プロジェクト管理';
const GANTT_SHEET_NAME = 'ガントチャート';
const SUMMARY_SHEET_NAME = '期日サマリー';

// 「プロジェクト管理」シートの列番号
const PRJ_NO_COL = 1, PRJ_TYPE_COL = 2, PRJ_CUSTOMER_NAME_COL = 3, PRJ_NAME_COL = 4, PRJ_ASSIGNEE_COL = 5,
      PRJ_STATUS_COL = 6, PRJ_PROGRESS_COL = 7,
      PRJ_START_DATE_COL = 8, PRJ_END_DATE_COL = 9, PRJ_LAST_UPDATED_COL = 10;
const HEADER_ROWS = 1;

// =============================================
// メイン機能
// =============================================
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('プロジェクト管理')
    .addItem('🤖 AIと対話して計画を作成', 'showAiDialog')
    .addItem('✅ 新規プロジェクトを追加', 'addNewProject')
    .addSeparator()
    .addItem('📘 選択行にマイルストーンを追加', 'addMilestone')
    .addItem('📝 選択行にタスクを追加', 'addTask')
    .addSeparator()
    .addItem('📊 ガントチャートを更新', 'updateGanttChart')
    .addItem('📅 期日サマリーを更新', 'updateDueDateSummary')
    .addSeparator()
    .addItem('📁 プロジェクト/マイルストーンを折りたたむ', 'menu_updateRowGroups')
    .addItem('📁 全てのグループ化を解除', 'menu_removeAllGroups')
    .addSeparator()
    .addItem('🎨 全行のデザインと色を再適用', 'recolorAllRowsByStatus')
    .addItem('📈 全ての進捗率を再計算', 'recalculateAllProgress')
    .addToUi();
}

function onEdit(e) {
  const range = e.range;
  const sheet = range.getSheet();
  const sheetName = sheet.getName();
  const editedRow = range.getRow();
  const editedCol = range.getColumn();
  if (sheetName !== PROJECT_SHEET_NAME || editedRow <= HEADER_ROWS) return;

  if (editedCol !== PRJ_LAST_UPDATED_COL && editedCol !== PRJ_NO_COL && editedCol !== PRJ_TYPE_COL) {
    sheet.getRange(editedRow, PRJ_LAST_UPDATED_COL).setValue(new Date());
  }
  if (editedCol === PRJ_STATUS_COL) {
    const rowRange = sheet.getRange(editedRow, 1, 1, sheet.getLastColumn());
    applyFormatting(rowRange, sheet.getRange(editedRow, PRJ_TYPE_COL).getValue());
    applyStatusColoring(rowRange, range.getValue());
  }
  if (editedCol === PRJ_PROGRESS_COL) {
    if (sheet.getRange(editedRow, PRJ_TYPE_COL).getValue() === 'タスク') {
      updateParentProgress(sheet, editedRow);
    }
  }
  if ([PRJ_NAME_COL, PRJ_PROGRESS_COL, PRJ_START_DATE_COL, PRJ_END_DATE_COL, PRJ_STATUS_COL].includes(editedCol)) {
    const lock = LockService.getScriptLock();
    if (lock.tryLock(100)) {
      try {
        SpreadsheetApp.getActiveSpreadsheet().toast('ガントチャートを自動更新中...', '更新中', 5);
        updateGanttChart(e);
      } finally {
        lock.releaseLock();
      }
    }
  }
}

function onChange(e) {
  const sheet = e.source.getSheetByName(PROJECT_SHEET_NAME);
  if (!sheet) return;
  if (e.changeType === 'REMOVE_ROW') {
    Utilities.sleep(1000);
    const maxRows = sheet.getMaxRows();
    const lastRow = sheet.getLastRow();
    if (maxRows > lastRow) { sheet.deleteRows(lastRow + 1, maxRows - lastRow); }
    updateRowGroups();
  }
}

// =============================================
// 行の折りたたみ(グループ化)機能
// =============================================
function menu_updateRowGroups() { updateRowGroups(); }
function menu_removeAllGroups() { removeAllGroups(); }

function updateRowGroups(sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(PROJECT_SHEET_NAME)) {
  if (!sheet) return;
  removeAllGroups(true, sheet);
  SpreadsheetApp.flush();
  const lastRow = sheet.getLastRow();
  if (lastRow <= HEADER_ROWS + 1) return;
  const types = sheet.getRange(HEADER_ROWS + 1, PRJ_TYPE_COL, lastRow - HEADER_ROWS, 1).getValues();
  for (let i = 0; i < types.length; i++) {
    const currentRow = i + HEADER_ROWS + 1;
    const currentType = types[i][0];
    if (currentType === 'プロジェクト' || currentType === 'マイルストーン') {
      let endOfGroupRow = lastRow;
      for (let j = i + 1; j < types.length; j++) {
        const nextType = types[j][0];
        const isSameOrHigherLevel =
          (currentType === 'プロジェクト' && nextType === 'プロジェクト') ||
          (currentType === 'マイルストーン' && (nextType === 'プロジェクト' || nextType === 'マイルストーン'));
        if (isSameOrHigherLevel) {
          endOfGroupRow = j + HEADER_ROWS;
          break;
        }
      }
      const startGroupRow = currentRow + 1;
      const numRowsToGroup = endOfGroupRow - startGroupRow + 1;
      if (numRowsToGroup > 0) {
        sheet.getRange(startGroupRow, 1, numRowsToGroup).shiftRowGroupDepth(1);
      }
    }
  }
}

function removeAllGroups(silent = false, sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(PROJECT_SHEET_NAME)) {
  if (!sheet) return;
  const lastRow = sheet.getLastRow();
  if (lastRow <= HEADER_ROWS) return;
  const range = sheet.getRange(HEADER_ROWS + 1, 1, lastRow - HEADER_ROWS);
  for (let i = 0; i < 10; i++) {
    try {
      range.ungroup();
    } catch (e) {
      break;
    }
  }
  if (!silent && sheet.getName() === PROJECT_SHEET_NAME) {
    SpreadsheetApp.getUi().alert('全てのグループ化を解除しました。');
  }
}

// =============================================
// AI関連の関数
// =============================================
function showAiDialog() { const html = HtmlService.createHtmlOutputFromFile('dialog').setWidth(600).setHeight(550); SpreadsheetApp.getUi().showModalDialog(html, 'AIと計画を作成'); }
function getInitialAiPlan(projectGoal, startDate, endDate) { const userProperties = PropertiesService.getUserProperties(); userProperties.setProperty('PROJECT_GOAL', projectGoal); userProperties.setProperty('PROJECT_START_DATE', startDate); userProperties.setProperty('PROJECT_END_DATE', endDate); return callGeminiAPI(createInitialPrompt(projectGoal, startDate, endDate), []); }
function requestAiRevision(currentPlanString, historyString, revisionRequest) { const userProperties = PropertiesService.getUserProperties(); const startDate = userProperties.getProperty('PROJECT_START_DATE'); const endDate = userProperties.getProperty('PROJECT_END_DATE'); return callGeminiAPI(createRevisionPrompt(currentPlanString, revisionRequest, startDate, endDate), JSON.parse(historyString)); }
function writePlanToSheet(finalPlanString) { addAiPlanToSheet(JSON.parse(finalPlanString), PropertiesService.getUserProperties().getProperty('PROJECT_GOAL')); }
function callGeminiAPI(userPrompt, history) { try { const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY'); if (!apiKey) { throw new Error('APIキーが設定されていません'); } const contents = history.map(turn => ({ role: turn.role, parts: [{ text: turn.text }] })); contents.push({ role: "user", parts: [{ text: userPrompt }] }); const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`; const payload = { contents: contents, generationConfig: { "responseMimeType": "application/json" } }; const options = { 'method': 'post', 'contentType': 'application/json', 'payload': JSON.stringify(payload), 'muteHttpExceptions': true }; const fetchResponse = UrlFetchApp.fetch(apiUrl, options); const responseBody = fetchResponse.getContentText(); if (fetchResponse.getResponseCode() !== 200) { throw new Error(`APIエラー: ${responseBody}`); } const responseObject = JSON.parse(responseBody); const planText = responseObject?.candidates?.[0]?.content?.parts?.[0]?.text; if (!planText) { throw new Error('AIからの応答に、期待されたテキスト部分が含まれていませんでした。'); } const plan = JSON.parse(planText); if (!plan || !plan.milestones) { throw new Error('AIからの応答形式が不正か、計画の生成に失敗しました。'); } const newHistory = [ ...history, { role: "user", text: userPrompt }, { role: "model", text: planText } ]; return { plan: plan, history: newHistory }; } catch (e) { Logger.log(`エラー詳細: ${e.stack}`); return { error: e.message }; } }
function createInitialPrompt(projectGoal, startDate, endDate) { return `あなたは優秀なプロジェクトマネージャーです。以下のプロジェクト計画について、現実的なマイルストーンと具体的なタスクを生成してください。\n# プロジェクト概要\n${projectGoal}\n# プロジェクト期間\n- 開始日: ${startDate}\n- 終了日: ${endDate}\n# 出力形式のルール\n- 必ず以下のJSON形式で出力してください。\n- 全てのマイルストーンとタスクには、必ず "start_date" と "end_date" を "YYYY-MM-DD" 形式で含めてください。\n- 全てのスケジュールは、上記のプロジェクト期間内に収まるように最適に計画してください。\n- 他のテキストは一切含めず、JSONオブジェクトのみを出力してください。\n\`\`\`json\n{\n  "project_name": "(AIが考えた適切なプロジェクト名)",\n  "milestones": [\n    {\n      "name": "(マイルストーン1の名前)",\n      "start_date": "YYYY-MM-DD",\n      "end_date": "YYYY-MM-DD",\n      "tasks": [\n        { "name": "(タスク1-1の名前)", "start_date": "YYYY-MM-DD", "end_date": "YYYY-MM-DD" }\n      ]\n    }\n  ]\n}\n\`\`\``; }
function createRevisionPrompt(currentPlanString, revisionRequest, startDate, endDate) { return `あなたは優秀なプロジェクトマネージャーです。\n以下の現在の計画案と全体期間を踏まえ、ユーザーからの修正指示に従って計画案を修正してください。\n# プロジェクト全体期間\n- 開始日: ${startDate}\n- 終了日: ${endDate}\n# 現在の計画案\n\`\`\`json\n${currentPlanString}\n\`\`\`\n# ユーザーからの修正指示\n${revisionRequest}\n# 出力形式のルール\n- 修正後の計画案「全体」を、必ず以前と同じJSON形式で出力してください。\n- 全てのマイルストーンとタスクの日付は、上記のプロジェクト全体期間内に収まるように再計画してください。\n- 他のテキストは一切含めず、JSONオブジェクトのみを出力してください。`; }

// =============================================
// 基本的な行操作・表示機能
// =============================================
function addAiPlanToSheet(plan, originalGoal) {
  // ▼▼▼ 【カスタマイズ箇所】 ▼▼▼
  // AIが生成したタスク名に特定のキーワードが含まれている場合に、担当者を自動で割り当てます。
  // ご自身のチームに合わせて、名前やキーワードを自由に変更・追加してください。
  const ASSIGNEES = {
    assigneeA: {
      name: '担当者A',
      keywords: ['調整', '連絡', '手配', '申請', '渉外', '契約', '募集', '会計', '精算', '挨拶', '許可', '保険', 'スタッフ', '警備', '救護', '受付', '案内', '誘導', '清掃']
    },
    assigneeB: {
      name: '担当者B',
      keywords: ['企画', 'コンセプト', 'デザイン', '広報', '作成', 'SNS', 'ウェブサイト', 'ポスター', 'チラシ', 'プレスリリース', '名称', 'コンテンツ', '告知', '発信']
    }
    // 例:3人目の担当者を追加する場合
    // , assigneeC: {
    //   name: '担当者C',
    //   keywords: ['分析', 'レポート', '調査', 'データ']
    // }
  };
  // ▲▲▲ 【カスタマイズ箇所】 ▲▲▲

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(PROJECT_SHEET_NAME);
  if (!sheet) return;
  const projectNo = findNextProjectNo(sheet);
  let newRowsData = [];
  const projectRow = [projectNo, 'プロジェクト', '', plan.project_name || originalGoal, '', '未着手', 0, plan.milestones?.[0]?.start_date || '', plan.milestones?.[plan.milestones.length - 1]?.end_date || '', new Date()];
  newRowsData.push(projectRow);
  plan.milestones.forEach((milestone, msIndex) => {
    const milestoneNo = `${projectNo}-${msIndex + 1}`;
    const milestoneName = '  ' + (milestone.name || '');
    newRowsData.push([milestoneNo, 'マイルストーン', '', milestoneName, '', '未着手', 0, milestone.start_date || '', milestone.end_date || '', new Date()]);
    if (milestone.tasks && Array.isArray(milestone.tasks)) {
      milestone.tasks.forEach((task, taskIndex) => {
        const taskNo = `${milestoneNo}-${taskIndex + 1}`;
        let assignedPerson = '';
        for (const key in ASSIGNEES) {
          if (ASSIGNEES[key].keywords.some(keyword => (task.name || '').includes(keyword))) {
            assignedPerson = ASSIGNEES[key].name;
            break;
          }
        }
        const taskName = '    ' + (task.name || '');
        newRowsData.push([taskNo, 'タスク', '', taskName, assignedPerson, '未着手', 0, task.start_date || '', task.end_date || '', new Date()]);
      });
    }
  });
  if (newRowsData.length === 0) return;
  const startRow = sheet.getLastRow() + 1;
  const numNewRows = newRowsData.length;
  const numCols = newRowsData[0].length;
  sheet.getRange(startRow, 1, numNewRows, numCols).setValues(newRowsData);
  sheet.getRange(startRow, PRJ_PROGRESS_COL, numNewRows, 1).setNumberFormat('0%');
  const backgrounds = [], fontWeights = [], fontColors = [];
  for (let i = 0; i < numNewRows; i++) {
    const type = newRowsData[i][PRJ_TYPE_COL - 1];
    const rowBackgrounds = new Array(sheet.getLastColumn()).fill(null);
    const rowFontWeights = new Array(sheet.getLastColumn()).fill('normal');
    const rowFontColors = new Array(sheet.getLastColumn()).fill(null);
    switch (type) {
      case 'プロジェクト': rowBackgrounds.fill('#434a54'); rowFontWeights.fill('bold'); rowFontColors.fill('#ffffff'); break;
      case 'マイルストーン': rowBackgrounds.fill('#aab2bd'); rowFontColors.fill('#ffffff'); break;
    }
    backgrounds.push(rowBackgrounds); fontWeights.push(rowFontWeights); fontColors.push(rowFontColors);
  }
  const newRange = sheet.getRange(startRow, 1, numNewRows, sheet.getLastColumn());
  newRange.setBackgrounds(backgrounds); newRange.setFontWeights(fontWeights); newRange.setFontColors(fontColors);
  newRange.setBorder(false, false, false, false, false, false);
  for (let i = 0; i < numNewRows; i++) {
    if (newRowsData[i][PRJ_TYPE_COL - 1] === 'プロジェクト') {
      sheet.getRange(startRow + i, 1, 1, sheet.getLastColumn()).setBorder(true, null, null, null, null, null, '#cccccc', SpreadsheetApp.BorderStyle.SOLID);
    }
  }
  recalculateAllProgress(); updateRowGroups(); SpreadsheetApp.flush(); sheet.getRange(startRow, PRJ_NAME_COL).activate();
}

function addNewProject() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(PROJECT_SHEET_NAME); if (!sheet) { return; } const nextRow = sheet.getLastRow() + 1; const newProjectNo = findNextProjectNo(sheet); const newRow = [newProjectNo, 'プロジェクト', '', '', '', '未着手', 0, '', '', new Date()]; sheet.appendRow(newRow); const newRowRange = sheet.getRange(nextRow, 1, 1, sheet.getLastColumn()); newRowRange.getCell(1, PRJ_PROGRESS_COL).setNumberFormat('0%'); applyFormatting(newRowRange, 'プロジェクト'); applyStatusColoring(newRowRange, '未着手'); sheet.getRange(nextRow, PRJ_CUSTOMER_NAME_COL).activate(); updateRowGroups(); }
function addMilestone() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(PROJECT_SHEET_NAME); const activeRange = sheet.getActiveRange(); if (!activeRange) { return; } const activeRow = activeRange.getRow(); if (activeRow <= HEADER_ROWS) return; const parentType = sheet.getRange(activeRow, PRJ_TYPE_COL).getValue(); let parentNo; if (parentType === 'プロジェクト') { parentNo = sheet.getRange(activeRow, PRJ_NO_COL).getValue().toString(); } else if (['マイルストーン', 'タスク'].includes(parentType)) { parentNo = sheet.getRange(activeRow, PRJ_NO_COL).getValue().toString().split('-').slice(0, 1).join('-'); } else { return; } const nextMilestoneNo = findNextChildNo(sheet, parentNo, 2); const insertionRow = findInsertionRow(sheet, activeRow); const parentCustomerName = getParentCustomerName(sheet, activeRow); sheet.insertRowBefore(insertionRow); const newRowValues = [nextMilestoneNo, 'マイルストーン', parentCustomerName, '  ', '未着手', 0, '', '', new Date()]; sheet.getRange(insertionRow, 1, 1, newRowValues.length).setValues([newRowValues]); const newRowRange = sheet.getRange(insertionRow, 1, 1, sheet.getLastColumn()); newRowRange.getCell(1, PRJ_PROGRESS_COL).setNumberFormat('0%'); applyFormatting(newRowRange, 'マイルストーン'); applyStatusColoring(newRowRange, '未着手'); sheet.getRange(insertionRow, PRJ_NAME_COL).activate(); updateParentProgress(sheet, insertionRow); updateRowGroups(); }
function addTask() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(PROJECT_SHEET_NAME); const activeRange = sheet.getActiveRange(); if (!activeRange) { return; } const activeRow = activeRange.getRow(); if (activeRow <= HEADER_ROWS) return; const parentType = sheet.getRange(activeRow, PRJ_TYPE_COL).getValue(); let parentNo; if (parentType === 'プロジェクト') { SpreadsheetApp.getUi().alert('プロジェクトには直接タスクを追加できません。'); return; } else if (['マイルストーン', 'タスク'].includes(parentType)) { parentNo = sheet.getRange(activeRow, PRJ_NO_COL).getValue().toString().split('-').slice(0, 2).join('-'); } else { return; } const nextTaskNo = findNextChildNo(sheet, parentNo, 3); const insertionRow = findInsertionRow(sheet, activeRow); const parentCustomerName = getParentCustomerName(sheet, activeRow); sheet.insertRowBefore(insertionRow); const newRowValues = [nextTaskNo, 'タスク', parentCustomerName, '    ', '未着手', 0, '', '', new Date()]; sheet.getRange(insertionRow, 1, 1, newRowValues.length).setValues([newRowValues]); const newRowRange = sheet.getRange(insertionRow, 1, 1, sheet.getLastColumn()); newRowRange.getCell(1, PRJ_PROGRESS_COL).setNumberFormat('0%'); applyFormatting(newRowRange, 'タスク'); applyStatusColoring(newRowRange, '未着手'); sheet.getRange(insertionRow, PRJ_NAME_COL).activate(); updateParentProgress(sheet, insertionRow); updateRowGroups(); }
function recolorAllRowsByStatus(sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(PROJECT_SHEET_NAME)) { if(!sheet) return; const lastRow = sheet.getLastRow(); if (lastRow <= HEADER_ROWS) return; const data = sheet.getRange(HEADER_ROWS + 1, 1, lastRow - HEADER_ROWS, sheet.getLastColumn()).getValues(); for (let i = 0; i < data.length; i++) { const row = HEADER_ROWS + 1 + i; const type = data[i][PRJ_TYPE_COL - 1]; const status = data[i][PRJ_STATUS_COL - 1]; const rowRange = sheet.getRange(row, 1, 1, sheet.getLastColumn()); applyFormatting(rowRange, type); applyStatusColoring(rowRange, status); } if (sheet.getName() === PROJECT_SHEET_NAME) { SpreadsheetApp.getUi().alert('全行のデザインと色の再適用が完了しました。'); } }
function recalculateAllProgress() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(PROJECT_SHEET_NAME); const lastRow = sheet.getLastRow(); if (lastRow <= HEADER_ROWS) return; const data = sheet.getRange(HEADER_ROWS + 1, 1, lastRow - HEADER_ROWS, PRJ_PROGRESS_COL).getValues(); data.forEach((row, i) => { if (row[PRJ_TYPE_COL - 1] === 'マイルストーン') { const milestoneNo = row[PRJ_NO_COL - 1].toString(); const childTasks = data.filter(d => d[PRJ_TYPE_COL - 1] === 'タスク' && d[PRJ_NO_COL - 1].toString().startsWith(milestoneNo + '-')); data[i][PRJ_PROGRESS_COL - 1] = calculateAverage(childTasks.map(t => t[PRJ_PROGRESS_COL - 1])); } }); data.forEach((row, i) => { if (row[PRJ_TYPE_COL - 1] === 'プロジェクト') { const projectNo = row[PRJ_NO_COL - 1].toString(); const childMilestones = data.filter(d => d[PRJ_TYPE_COL - 1] === 'マイルストーン' && d[PRJ_NO_COL - 1].toString().startsWith(projectNo + '-')); data[i][PRJ_PROGRESS_COL - 1] = calculateAverage(childMilestones.map(m => m[PRJ_PROGRESS_COL - 1])); } }); const progressData = data.map(row => [row[PRJ_PROGRESS_COL - 1]]); sheet.getRange(HEADER_ROWS + 1, PRJ_PROGRESS_COL, progressData.length, 1).setValues(progressData).setNumberFormat('0%'); }
function updateParentProgress(sheet, startRow) { const no = sheet.getRange(startRow, PRJ_NO_COL).getValue().toString(); const noParts = no.split('-'); if (noParts.length <= 1) return; const parentNo = noParts.slice(0, -1).join('-'); const parentRow = findRowByNo(sheet, parentNo); if (!parentRow) return; const siblings = findChildren(sheet, parentNo); const average = calculateAverage(siblings.map(row => sheet.getRange(row, PRJ_PROGRESS_COL).getValue())); sheet.getRange(parentRow, PRJ_PROGRESS_COL).setValue(average).setNumberFormat('0%'); updateParentProgress(sheet, parentRow); }
function getParentCustomerName(sheet, startRow) { for (let i = startRow; i > HEADER_ROWS; i--) { if (sheet.getRange(i, PRJ_TYPE_COL).getValue() === 'プロジェクト') { return sheet.getRange(i, PRJ_CUSTOMER_NAME_COL).getValue(); } } return ''; }
function findNextProjectNo(sheet) { const lastRow = sheet.getLastRow(); if (lastRow < HEADER_ROWS + 1) return 1; const allNos = sheet.getRange(HEADER_ROWS + 1, PRJ_NO_COL, lastRow - HEADER_ROWS, 1).getValues(); let maxProjectNo = 0; allNos.forEach(row => { const noStr = row[0].toString(); if (noStr && !noStr.includes('-')) { const no = parseInt(noStr, 10); if (no > maxProjectNo) { maxProjectNo = no; } } }); return maxProjectNo + 1; }
function findNextChildNo(sheet, parentNo, targetLevel) { const lastRow = sheet.getLastRow(); if (lastRow < HEADER_ROWS + 1) return `${parentNo}-1`; const allNos = sheet.getRange(HEADER_ROWS + 1, PRJ_NO_COL, lastRow - HEADER_ROWS, 1).getValues(); let maxChildNo = 0; const prefix = parentNo + '-'; allNos.forEach(row => { const noStr = row[0].toString(); if (noStr.startsWith(prefix) && noStr.split('-').length === targetLevel) { const childPart = parseInt(noStr.split('-')[targetLevel - 1], 10); if (childPart > maxChildNo) { maxChildNo = childPart; } } }); return `${parentNo}-${maxChildNo + 1}`; }
function findInsertionRow(sheet, startRow) { const startNo = sheet.getRange(startRow, PRJ_NO_COL).getValue().toString(); const startLevel = startNo.split('-').length; for (let i = startRow + 1; i <= sheet.getLastRow(); i++) { const currentRowNo = sheet.getRange(i, PRJ_NO_COL).getValue().toString(); if (currentRowNo) { const currentLevel = currentRowNo.split('-').length; if (currentLevel <= startLevel) { return i; } } } return sheet.getLastRow() + 1; }
function findRowByNo(sheet, noToFind) { const allNos = sheet.getRange(HEADER_ROWS + 1, PRJ_NO_COL, sheet.getLastRow() - HEADER_ROWS, 1).getValues(); for (let i = 0; i < allNos.length; i++) { if (allNos[i][0].toString() === noToFind) { return i + HEADER_ROWS + 1; } } return null; }
function findChildren(sheet, parentNo) { const childRows = []; const parentLevel = parentNo.split('-').length; const allNos = sheet.getRange(HEADER_ROWS + 1, PRJ_NO_COL, sheet.getLastRow() - HEADER_ROWS, 1).getValues(); for (let i = 0; i < allNos.length; i++) { const currentNo = allNos[i][0].toString(); if (currentNo.startsWith(parentNo + '-') && currentNo.split('-').length === parentLevel + 1) { childRows.push(i + HEADER_ROWS + 1); } } return childRows; }
function calculateAverage(arr) { if (!arr || arr.length === 0) return 0; const sum = arr.reduce((acc, val) => acc + (typeof val === 'number' ? val : 0), 0); return sum / arr.length; }
function applyFormatting(range, type) { range.setBorder(false, false, false, false, false, false).setBackground(null).setFontWeight('normal').setFontColor(null); switch (type) { case 'プロジェクト': range.setFontWeight('bold').setBackground('#434a54').setFontColor('#ffffff').setBorder(true, null, null, null, null, null, '#cccccc', SpreadsheetApp.BorderStyle.SOLID); break; case 'マイルストーン': range.setBackground('#aab2bd').setFontColor('#ffffff'); break; } }
function applyStatusColoring(rowRange, status) { const type = rowRange.getSheet().getRange(rowRange.getRow(), PRJ_TYPE_COL).getValue(); if (type === 'プロジェクト' || type === 'マイルストーン') { return; } let color = null; switch (status) { case '完了': color = '#d9ead3'; break; case '作業中': color = '#d0e0e3'; break; case '確認中': color = '#fff2cc'; break; case '保留': color = '#fce5cd'; break; default: color = null; } if (type === 'タスク') { rowRange.setBackground(color); } }

// =============================================
// 期日サマリー機能
// =============================================
function updateDueDateSummary() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const projectSheet = ss.getSheetByName(PROJECT_SHEET_NAME);
  if (!projectSheet) { SpreadsheetApp.getUi().alert(`シート「${PROJECT_SHEET_NAME}」が見つかりません。`); return; }
  let summarySheet = ss.getSheetByName(SUMMARY_SHEET_NAME);
  if (!summarySheet) { summarySheet = ss.insertSheet(SUMMARY_SHEET_NAME); }
  summarySheet.clear();
  summarySheet.setFrozenRows(1);
  const sourceData = projectSheet.getDataRange().getValues();
  const projectSheetId = projectSheet.getSheetId();
  const today = new Date(); today.setHours(0, 0, 0, 0);
  const startOfWeek = new Date(today); startOfWeek.setDate(today.getDate() - today.getDay());
  const endOfWeek = new Date(startOfWeek); endOfWeek.setDate(startOfWeek.getDate() + 6);
  const todayTasks = [], thisWeekTasks = [];
  let currentProjectName = '';
  for (let i = HEADER_ROWS; i < sourceData.length; i++) {
    const row = sourceData[i];
    const type = row[PRJ_TYPE_COL - 1];
    if (type === 'プロジェクト') { currentProjectName = row[PRJ_NAME_COL - 1]; continue; }
    const dueDate = new Date(row[PRJ_END_DATE_COL - 1]);
    if (type === 'タスク' && dueDate instanceof Date && !isNaN(dueDate)) {
      dueDate.setHours(0, 0, 0, 0);
      const taskData = { no: row[PRJ_NO_COL - 1], name: row[PRJ_NAME_COL - 1].trim(), assignee: row[PRJ_ASSIGNEE_COL - 1], dueDate: dueDate, project: currentProjectName, rowNum: i + 1 };
      if (dueDate.getTime() === today.getTime()) { todayTasks.push(taskData); }
      else if (dueDate >= startOfWeek && dueDate <= endOfWeek) { thisWeekTasks.push(taskData); }
    }
  }
  let currentRow = 1;
  currentRow = writeSummarySection(summarySheet, currentRow, '本日が期日のタスク', todayTasks, projectSheetId);
  currentRow = writeSummarySection(summarySheet, currentRow, '今週中(今日を除く)が期日のタスク', thisWeekTasks, projectSheetId);
  summarySheet.autoResizeColumns(1, 6);
  SpreadsheetApp.getUi().alert('期日サマリーの更新が完了しました。');
}

function writeSummarySection(sheet, startRow, title, tasks, sourceSheetId) {
  let currentRow = startRow;
  sheet.getRange(currentRow, 1, 1, 6).merge().setValue(title).setFontWeight('bold').setBackground('#f3f3f3').setHorizontalAlignment('center');
  currentRow++;
  const headers = ['No.', 'プロジェクト', 'タスク名', '担当者', '期日', 'リンク'];
  sheet.getRange(currentRow, 1, 1, headers.length).setValues([headers]).setFontWeight('bold');
  currentRow++;
  if (tasks.length === 0) {
    sheet.getRange(currentRow, 1, 1, 6).merge().setValue('該当するタスクはありません。').setHorizontalAlignment('center');
    currentRow++;
  } else {
    const taskRows = tasks.map(task => {
      const linkFormula = `=HYPERLINK("#gid=${sourceSheetId}&range=A${task.rowNum}", "詳細へ")`;
      return [task.no, task.project, task.name, task.assignee, task.dueDate, linkFormula];
    });
    sheet.getRange(currentRow, 1, taskRows.length, taskRows[0].length).setValues(taskRows).setNumberFormat('yyyy/mm/dd');
    currentRow += taskRows.length;
  }
  return currentRow + 1;
}

// =============================================
// ガントチャート機能
// =============================================
function getJapaneseHolidays(year) { const holidays = new Set(); try { const calendar = CalendarApp.getCalendarById('ja.japanese#holiday@group.v.calendar.google.com'); const events = calendar.getEvents(new Date(year - 1, 11, 31), new Date(year + 1, 0, 1)); for (const event of events) { holidays.add(Utilities.formatDate(event.getStartTime(), 'JST', 'yyyy-MM-dd')); } } catch (e) { Logger.log('日本の祝日カレンダーの取得に失敗しました: ' + e.message); } return holidays; }
function columnToLetter(column) { let temp, letter = ''; while (column > 0) { temp = (column - 1) % 26; letter = String.fromCharCode(temp + 65) + letter; column = (column - temp - 1) / 26; } return letter; }
function updateGanttChart(e) { const ss = SpreadsheetApp.getActiveSpreadsheet(); const projectSheet = ss.getSheetByName(PROJECT_SHEET_NAME); let ganttSheet = ss.getSheetByName(GANTT_SHEET_NAME); if (!ganttSheet) { ganttSheet = ss.insertSheet(GANTT_SHEET_NAME); } ganttSheet.clear(); ganttSheet.setFrozenColumns(PRJ_NAME_COL); const sourceDataRange = projectSheet.getDataRange(); const sourceData = sourceDataRange.getValues(); ganttSheet.getRange(1, 1, sourceData.length, sourceData[0].length).setValues(sourceData); recolorAllRowsByStatus(ganttSheet); updateRowGroups(ganttSheet); const ganttChartStartCol = PRJ_LAST_UPDATED_COL + 1; let projectMinDate = null, projectMaxDate = null; for (let i = 1; i < sourceData.length; i++) { const startDate = new Date(sourceData[i][PRJ_START_DATE_COL - 1]); const endDate = new Date(sourceData[i][PRJ_END_DATE_COL - 1]); if (startDate instanceof Date && !isNaN(startDate)) { if (projectMinDate === null || startDate < projectMinDate) projectMinDate = startDate; } if (endDate instanceof Date && !isNaN(endDate)) { if (projectMaxDate === null || endDate > projectMaxDate) projectMaxDate = endDate; } } if (!projectMinDate || !projectMaxDate) { if(!e) SpreadsheetApp.getUi().alert('ガントチャートを作成するための開始日または終了日がプロジェクト管理シートに入力されていません。'); return; } const simulationDateCell = projectSheet.getRange('M1'); const simulationDate = new Date(simulationDateCell.getValue()); let today = (simulationDate instanceof Date && !isNaN(simulationDate)) ? simulationDate : new Date(); today.setHours(0, 0, 0, 0); let minDate = new Date(today); minDate.setDate(today.getDate() - 5); let maxDate = new Date(projectMaxDate > today ? projectMaxDate : today); maxDate.setDate(maxDate.getDate() + 45); const holidays = getJapaneseHolidays(minDate.getFullYear()); const dateHeaders = [], dayHeaders = [], backgroundColors = []; let currentDate = new Date(minDate); while (currentDate <= maxDate) { dateHeaders.push(new Date(currentDate)); dayHeaders.push(['日', '月', '火', '水', '木', '金', '土'][currentDate.getDay()]); let bgColor = '#ffffff'; const dayOfWeek = currentDate.getDay(); const isHoliday = holidays.has(Utilities.formatDate(currentDate, 'JST', 'yyyy-MM-dd')); if (dayOfWeek === 0 || isHoliday) bgColor = '#fbe9e7'; else if (dayOfWeek === 6) bgColor = '#e3f2fd'; backgroundColors.push(bgColor); currentDate.setDate(currentDate.getDate() + 1); } ganttSheet.getRange(1, ganttChartStartCol, 1, dateHeaders.length).setValues([dateHeaders]).setNumberFormat('m/d'); ganttSheet.getRange(2, ganttChartStartCol, 1, dayHeaders.length).setValues([dayHeaders]); ganttSheet.getRange(3, ganttChartStartCol, 1, backgroundColors.length).setBackgrounds([backgroundColors]); const diffDays = Math.floor((today.getTime() - minDate.getTime()) / (1000 * 60 * 60 * 24)); if (diffDays >= 0 && diffDays < dateHeaders.length) { ganttSheet.getRange(1, ganttChartStartCol + diffDays, ganttSheet.getMaxRows()).setBorder(null, true, null, null, null, null, '#ff0000', SpreadsheetApp.BorderStyle.SOLID_THICK); } const numRows = sourceData.length; const numCols = dateHeaders.length; if (numRows <= HEADER_ROWS + 1) { if(!e) SpreadsheetApp.getUi().alert('ガントチャートの描画対象となるデータがありません。'); return; } const chartRange = ganttSheet.getRange(HEADER_ROWS + 2, ganttChartStartCol, numRows - (HEADER_ROWS + 1), numCols); ganttSheet.clearConditionalFormatRules(); const startLetter = columnToLetter(ganttChartStartCol); const rules = [ { type: 'プロジェクト', color: '#434a54', progressColor: '#21252b' }, { type: 'マイルストーン', color: '#aab2bd', progressColor: '#788293' }, { type: 'タスク', status: '完了',   color: '#d9ead3', progressColor: '#6aa84f' }, { type: 'タスク', status: '作業中', color: '#d0e0e3', progressColor: '#4a86e8' }, { type: 'タスク', status: '確認中', color: '#fff2cc', progressColor: '#f1c232' }, { type: 'タスク', status: '保留',   color: '#fce5cd', progressColor: '#e69138' }, { type: 'タスク', status: '未着手', color: '#f3f3f3', progressColor: '#999999' } ]; const newRules = []; const firstDataRow = HEADER_ROWS + 2; for (const rule of rules) { let baseCondition = `$B${firstDataRow}="${rule.type}"`; if (rule.status) { baseCondition += `, $F${firstDataRow}="${rule.status}"`; } const progressFormula = `=AND(${baseCondition}, $H${firstDataRow}<>"", $I${firstDataRow}<>"", ${startLetter}$1>=$H${firstDataRow}, ${startLetter}$1<=$H${firstDataRow} + ($I${firstDataRow}-$H${firstDataRow}+1)*$G${firstDataRow})`; newRules.push(SpreadsheetApp.newConditionalFormatRule().whenFormulaSatisfied(progressFormula).setBackground(rule.progressColor).setRanges([chartRange]).build()); const totalFormula = `=AND(${baseCondition}, $H${firstDataRow}<>"", $I${firstDataRow}<>"", ${startLetter}$1>=$H${firstDataRow}, ${startLetter}$1<=$I${firstDataRow})`; newRules.push(SpreadsheetApp.newConditionalFormatRule().whenFormulaSatisfied(totalFormula).setBackground(rule.color).setRanges([chartRange]).build()); } ganttSheet.setConditionalFormatRules(newRules); ganttSheet.setRowHeightsForced(3, numRows - 1, 21); ganttSheet.setRowHeight(1, 21); ganttSheet.setRowHeight(2, 21); ganttSheet.autoResizeColumns(1, ganttChartStartCol - 1); ganttSheet.setColumnWidths(ganttChartStartCol, numCols, 35); if (numCols > 1) { const rangeToGroup = ganttSheet.getRange(1, ganttChartStartCol, 1, numCols); rangeToGroup.activate(); try { if(rangeToGroup.getColumnGroup(1,1) == null) { rangeToGroup.collapseGroups(); } } catch(e) { Logger.log("Could not collapse column group: " + e.message); } } if (SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getName() !== GANTT_SHEET_NAME) { ss.setActiveSheet(ganttSheet); } if (diffDays > 5) { ganttSheet.setActiveRange(ganttSheet.getRange(3, ganttChartStartCol + diffDays - 5)); } else if (diffDays >= 0) { ganttSheet.setActiveRange(ganttSheet.getRange(3, ganttChartStartCol + diffDays)); } else { ganttSheet.setActiveRange(ganttSheet.getRange(3, ganttChartStartCol)); } if (!e) { SpreadsheetApp.getUi().alert('ガントチャートの更新が完了しました。'); } }


2. dialog.html の設定

  • スクリプトエディタの左側にある「ファイル」の横の + アイコンをクリックし、HTML を選択します。

  • ファイル名に「dialog」と入力して Enter キーを押します。(.htmlは自動でつきます)

  • 新しくできた dialog.html の中身をすべて削除します。

  • 以下のコード全文をコピーして、空になった dialog.html に貼り付けます。

▼▼▼ [dialog.html] に貼り付けるコード (全文) ▼▼▼

codeHtml

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
    <style>
      body { padding: 20px; font-family: sans-serif; }
      .section { margin-bottom: 20px; }
      label { font-weight: bold; display: block; margin-bottom: 5px; }
      input[type="text"], input[type="date"], textarea { width: 100%; box-sizing: border-box; padding: 6px; border: 1px solid #ccc; border-radius: 4px; }
      .hidden { display: none; }
      #plan-display { background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 4px; padding: 15px; height: 250px; overflow-y: auto; margin-bottom: 15px; }
      #plan-display h4 { margin-top: 0; margin-bottom: 15px; padding-bottom: 8px; border-bottom: 2px solid #ddd; font-size: 1.1em; color: #333; }
      #plan-display p { margin-top: 12px; margin-bottom: 5px; }
      #plan-display ul { list-style-type: disc; margin-top: 5px; margin-bottom: 15px; padding-left: 25px; }
      #plan-display li { margin-bottom: 5px; line-height: 1.4; }
      .date-span { font-size: 0.85em; color: #777; margin-left: 8px; }
      .button-bar { text-align: right; margin-top: 20px; }
      #loader { text-align: center; padding: 10px; font-weight: bold; color: #555; display: none; }
    </style>
  </head>
  <body>
    <!-- 初期入力画面 -->
    <div id="initial-input-screen">
      <h3>AIによるプロジェクト計画作成</h3>
      <div class="section">
        <label for="project-goal">プロジェクトの概要</label>
        <textarea id="project-goal" rows="4" placeholder="例:新規カフェの立ち上げ"></textarea>
      </div>
      <div class="section">
        <label for="start-date">プロジェクト開始日</label>
        <input type="date" id="start-date">
      </div>
      <div class="section">
        <label for="end-date">プロジェクト終了日</label>
        <input type="date" id="end-date">
      </div>
      <div class="button-bar">
        <button id="start-ai-button" class="action">計画作成を開始</button>
      </div>
    </div>

    <!-- AIとの対話画面 (最初は非表示) -->
    <div id="conversation-screen" class="hidden">
       <h3>AIによるプロジェクト計画</h3>
       <p>AIが生成した以下の計画案を確認し、修正指示を入力するか、シートへの反映を決定してください。</p>
       <div id="plan-display"><div id="loader-initial" style="display: block; text-align: center; padding: 10px; font-weight: bold; color: #555;">計画を生成中です...</div></div>
       <label for="revision-request">修正・追加の指示:</label>
       <textarea id="revision-request" placeholder="例:フェーズ1の期間をもう少し短くして。"></textarea>
       <div id="loader">AIが考え中です...</div>
       <div class="button-bar">
         <button id="cancel-button" class="gray">キャンセル</button>
         <button id="revise-button">この指示で修正を依頼</button>
         <button id="reflect-button" class="action">この内容でシートに反映</button>
       </div>
    </div>

    <script>
      let currentPlan = null; let conversationHistory = [];

      document.getElementById('start-ai-button').addEventListener('click', function() {
        const goal = document.getElementById('project-goal').value;
        const start = document.getElementById('start-date').value;
        const end = document.getElementById('end-date').value;
        if (!goal.trim() || !start || !end) { alert('すべての項目を入力してください。'); return; }
        document.getElementById('initial-input-screen').classList.add('hidden');
        document.getElementById('conversation-screen').classList.remove('hidden');
        google.script.run.withSuccessHandler(updateDialog).getInitialAiPlan(goal, start, end);
      });

      function updateDialog(response) {
        setLoading(false);
        const displayDiv = document.getElementById('plan-display');
        if (response.error) { displayDiv.innerHTML = '<p style="color: red; font-weight: bold;">エラー: ' + escapeHtml(response.error) + '</p>'; return; }
        currentPlan = response.plan; conversationHistory = response.history;
        let html = '';
        if (currentPlan.project_name) { html += '<h4>' + escapeHtml(currentPlan.project_name) + '</h4>'; }
        if (currentPlan.milestones && Array.isArray(currentPlan.milestones)) {
          currentPlan.milestones.forEach(function(milestone) {
            let milestoneHtml = '<p><strong>' + escapeHtml(milestone.name) + '</strong>';
            if (milestone.start_date && milestone.end_date) { milestoneHtml += '<span class="date-span">(' + escapeHtml(milestone.start_date) + ' ~ ' + escapeHtml(milestone.end_date) + ')</span>'; }
            milestoneHtml += '</p>';
            html += milestoneHtml;
            if (milestone.tasks && Array.isArray(milestone.tasks)) {
              html += '<ul>';
              milestone.tasks.forEach(function(task) {
                let taskHtml = '<li>' + escapeHtml(task.name);
                if (task.start_date && task.end_date) { taskHtml += '<span class="date-span">(' + escapeHtml(task.start_date) + ' ~ ' + escapeHtml(task.end_date) + ')</span>'; }
                taskHtml += '</li>';
                html += taskHtml;
              });
              html += '</ul>';
            }
          });
        }
        displayDiv.innerHTML = html;
        document.getElementById('revision-request').value = '';
      }
      
      function escapeHtml(str) { const div = document.createElement('div'); div.appendChild(document.createTextNode(str || '')); return div.innerHTML; }
      document.getElementById('revise-button').addEventListener('click', function() { const req = document.getElementById('revision-request').value; if (!req.trim()) { alert('修正指示を入力してください。'); return; } setLoading(true); google.script.run.withSuccessHandler(updateDialog).requestAiRevision(JSON.stringify(currentPlan), JSON.stringify(conversationHistory), req); });
      document.getElementById('reflect-button').addEventListener('click', function() { setLoading(true); google.script.run.withSuccessHandler(function(){ google.script.host.close(); }).writePlanToSheet(JSON.stringify(currentPlan)); });
      document.getElementById('cancel-button').addEventListener('click', function() { google.script.host.close(); });
      function setLoading(isLoading) { document.getElementById('loader').style.display = isLoading ? 'block' : 'none'; document.getElementById('revise-button').disabled = isLoading; document.getElementById('reflect-button').disabled = isLoading; }
    </script>
  </body>
</html>

貼り付けが終わったら、必ず両方のファイルを保存してください。(フロッピーディスクのアイコンをクリック)

ステップ4:Gemini APIキーの取得と設定

これが一番の山場ですが、落ち着いて進めれば簡単です。AIに指示を出すための「APIキー」というパスワードのようなものを取得します。

  1. Google AI Studio にアクセスし、Googleアカウントでログインします。

  2. API キーを作成」をクリックし、表示されるプロジェクトでAPIキーを作成します。

  3. APIキー(長い英数字の羅列)が生成されるので、コピーします。
    ※このキーはパスワードと同じです。絶対に他人に教えたり、SNSなどで公開したりしないでください。

  4. スプレッドシートのApps Script画面に戻ります。

  5. 左側のメニューから歯車アイコンの「プロジェクトの設定」をクリックします。

  6. 一番下にある「スクリプト プロパティ」の「スクリプト プロパティを追加」をクリックします。

  7. 以下の通り、一字一句間違えないように入力してください。

    • プロパティ: GEMINI_API_KEY

    • : 先ほどコピーしたAPIキーを貼り付け

  8. スクリプト プロパティを保存」をクリックします。

画像

ステップ5:初回実行と承認

あと少しです!最後に、作成したスクリプトにスプレッドシートを操作する許可を与えます。

  1. 設定が終わったら、スプレッドシートのタブに戻り、ページを再読み込み(リロード)してください。

  2. メニューに「プロジェクト管理」という項目が追加されているはずです。

  3. プロジェクト管理 > 🤖 AIと対話して計画を作成 をクリックします。

  4. 初めて実行する際は「承認が必要です」というダイアログが表示されます。これは、あなたが作ったプログラムに「スプレッドシートを編集していいですよ」と許可を与えるための、ごく普通の操作です。

  5. 権限を確認 → 自分のGoogleアカウントを選択 → 詳細 → (安全ではないページ)に移動 → 許可 の順にクリックしてください。

これで、すべての設定が完了です!


ツールの使い方

使い方はとても簡単です。

  1. メニューの プロジェクト管理 > 🤖 AIと対話して計画を作成 をクリック。

  2. 表示されたウィンドウに「プロジェクトの概要」「開始日」「終了日」を入力して、「計画作成を開始」ボタンを押します。

  3. AIが計画案を生成します。修正したい点があれば、下のテキストボックスに指示を入力して「この指示で修正を依頼」をクリックします。

  4. 計画案に満足したら、「この内容でシートに反映」ボタンを押すだけ!

自動でタスクがシートに追加され、ガントチャートや期日サマリーも作成されます。


【応用編】担当者の自動割り振りをカスタマイズしよう!

このツールには、AIが作ったタスク名に応じて担当者を自動で割り振る機能がついています。あなたのチームに合わせて設定を変更してみましょう!

  1. 拡張機能 > Apps Script を開きます。

  2. コード.gs の中から、addAiPlanToSheet という部分を探します。

  3. その中にある 【カスタマイズ箇所】 と書かれた部分を、あなたのチームメンバーの名前に書き換えてください。

書き換え例:
「担当者A」を「佐藤さん」、「担当者B」を「田中さん」に変更し、新しく「鈴木さん」を追加する場合。

codeJavaScript

// ▼▼▼ 【カスタマイズ箇所】 ▼▼▼
  const ASSIGNEES = {
    sato: {
      name: '佐藤',
      keywords: ['調整', '連絡', '手配', '申請', '渉外', '契約', '募集', '会計'] // 佐藤さんが担当するタスクのキーワード
    },
    tanaka: {
      name: '田中',
      keywords: ['企画', 'コンセプト', 'デザイン', '広報', '作成', 'SNS'] // 田中さんが担当するタスクのキーワード
    },
    // ↓新しく鈴木さんを追加
    suzuki: {
      name: '鈴木',
      keywords: ['分析', 'レポート', '調査', 'データ'] // 鈴木さんが担当するタスクのキーワード
    }
  };
  // ▲▲▲ 【カスタマイズ箇所】 ▲▲▲

name にはシートに表示したい名前を、keywords にはその人が担当する作業に関連するキーワードを好きなだけ追加できます。これで、あなたのチーム専用の自動化ツールが完成します!

おわりに

今回は、GoogleスプレッドシートとGemini AIを連携させて、プロジェクト管理を自動化する方法をご紹介しました。

面倒な作業はAIに任せて、私たちはもっとクリエイティブな仕事に時間を使っていきましょう。


ぜひ、あなたのプロジェクト管理にこのツールを活用してみてください!
そしてぜひ感想をお寄せください!

いいなと思ったら応援しよう!

ピックアップされています

マガジン1

  • 285本

ビジネス系

  • 75本

後で読む

  • 11本

コメント

1
コメントするには、 ログイン または 会員登録 をお願いします。
賃貸亭のーぷらんのプロフィールへのリンク

チームみらいみたいに、 三郷市での行政DXや立法DXの取り組みのお話を聞きたいな あと、X-GUNの西尾さんは頑張っていますか?

1
鈴木 優作 | 三郷市議会議員 いいね
【コピペでOK】Googleスプレッドシートが最強のAIプロジェクト管理ツールに変身!Gemini APIでタスク管理を自動化しよう|鈴木 優作 | 三郷市議会議員
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1