1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GASで職員会議の議題フォームを会議アジェンダに自動整形する

1
Posted at

この記事では、Googleフォームで集めた職員会議の議題を、会議用アジェンダシートに自動整形する手順がわかります。
「期限後の議題」「担当未入力」「同じような議題」を、会議前に見つける実装がわかります。
GASだけで、入力受付からアジェンダ生成、確認メール文面の作成までを小さくつなげる方法がわかります。

課題

職員会議の議題をフォームで集めると、回収自体は楽になります。

ただ、そのままスプレッドシートに回答が積み上がるだけだと、会議前に次の作業が残ります。

  • 議題をカテゴリ別に並べ替える
  • 担当者が空欄の回答を探す
  • 似た議題をまとめる
  • 期限後に届いたものを分ける
  • 当日配るアジェンダ用の形に整える

既出記事との重複も避けます。公開済みのQiitaには、たとえば「GASでフォーム回答から『未対応だけ』の職員用ToDo表を自動生成する」「GASでフォーム回答を週次ふり返りレポートに整える:AIに渡す前の集計テンプレート」「GASでフォーム回答から個別フィードバック下書きを作る: 校務で使えるAI前処理パターン」があります。

この記事では、フォーム回答の「未対応ToDo化」や「AI前処理」ではなく、職員会議の議題を会議アジェンダに整形するところに絞ります。

原因

フォーム回答のシートは、入力された時系列のログです。

一方で、会議アジェンダに必要なのは、会議で扱う順番に並んだ一覧です。

つまり、入力に向いた表と、会議に向いた表が違います。

そこで、回答シートを直接きれいにしようとせず、次の3層に分けます。

  • Form Responses:フォーム回答の生データ
  • Agenda:会議用に整形した一覧
  • Issues:確認が必要な行だけを集めた一覧

この分け方にすると、フォーム回答を壊さず、会議前に必要な確認だけを見つけやすくなります。

実装

前提として、Googleフォームの回答先スプレッドシートに次の列があるとします。

内容
A タイムスタンプ
B 議題
C カテゴリ
D 担当
E 希望扱い
F 補足

カテゴリは 報告相談決裁共有 のように、フォーム側で選択式にしておくと後工程が安定します。

1. メニューを追加する

スプレッドシートを開いたときに、職員会議用のメニューを出します。

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('職員会議')
    .addItem('アジェンダを更新', 'buildMeetingAgenda')
    .addItem('確認事項を更新', 'buildIssueList')
    .addToUi();
}

この段階では、まだ自動送信や外部API連携はしません。

まずは、職員室内で確認できるシートを作るだけにします。

2. 回答をアジェンダ用に整形する

次のコードでは、フォーム回答を読み取り、カテゴリ順と投稿時刻順で並べて Agenda シートに出力します。

const CONFIG = {
  responseSheetName: 'Form Responses',
  agendaSheetName: 'Agenda',
  issueSheetName: 'Issues',
  deadline: '2026-09-01T17:00:00+09:00',
  categoryOrder: ['決裁', '相談', '報告', '共有']
};

function buildMeetingAgenda() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const responseSheet = ss.getSheetByName(CONFIG.responseSheetName);
  if (!responseSheet) {
    throw new Error(`シートが見つかりません: ${CONFIG.responseSheetName}`);
  }

  const values = responseSheet.getDataRange().getValues();
  const rows = values.slice(1).map(toAgendaRow).filter(row => row.topic);

  rows.sort((a, b) => {
    const categoryDiff = categoryRank(a.category) - categoryRank(b.category);
    if (categoryDiff !== 0) return categoryDiff;
    return a.timestamp.getTime() - b.timestamp.getTime();
  });

  const agendaSheet = getOrCreateSheet_(ss, CONFIG.agendaSheetName);
  agendaSheet.clear();
  agendaSheet.getRange(1, 1, 1, 6).setValues([[
    'No.',
    'カテゴリ',
    '議題',
    '担当',
    '希望扱い',
    '補足'
  ]]);

  if (rows.length === 0) return;

  const output = rows.map((row, index) => [
    index + 1,
    row.category,
    row.topic,
    row.owner,
    row.requestType,
    row.note
  ]);

  agendaSheet.getRange(2, 1, output.length, output[0].length).setValues(output);
  agendaSheet.autoResizeColumns(1, 6);
}

function toAgendaRow(row) {
  return {
    timestamp: new Date(row[0]),
    topic: String(row[1] || '').trim(),
    category: String(row[2] || '共有').trim(),
    owner: String(row[3] || '').trim(),
    requestType: String(row[4] || '').trim(),
    note: String(row[5] || '').trim()
  };
}

function categoryRank(category) {
  const index = CONFIG.categoryOrder.indexOf(category);
  return index === -1 ? CONFIG.categoryOrder.length : index;
}

function getOrCreateSheet_(ss, name) {
  return ss.getSheetByName(name) || ss.insertSheet(name);
}

この実装のポイントは、回答シートを加工しないことです。

会議用の見た目は Agenda に作り直すので、フォーム回答の履歴を残したまま、会議で使う順番だけを変えられます。

3. 確認が必要な行を抽出する

次に、期限後の投稿、担当者未入力、似た議題の疑いを Issues に集めます。

function buildIssueList() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const responseSheet = ss.getSheetByName(CONFIG.responseSheetName);
  if (!responseSheet) {
    throw new Error(`シートが見つかりません: ${CONFIG.responseSheetName}`);
  }

  const values = responseSheet.getDataRange().getValues();
  const rows = values.slice(1).map(toAgendaRow).filter(row => row.topic);
  const deadline = new Date(CONFIG.deadline);
  const issues = [];

  rows.forEach((row, index) => {
    const rowNumber = index + 2;

    if (!row.owner) {
      issues.push([rowNumber, '担当未入力', row.topic, row.category, row.owner, row.requestType]);
    }

    if (row.timestamp.getTime() > deadline.getTime()) {
      issues.push([rowNumber, '期限後投稿', row.topic, row.category, row.owner, row.requestType]);
    }
  });

  const duplicatePairs = findSimilarTopicPairs_(rows);
  duplicatePairs.forEach(pair => {
    issues.push([
      `${pair.aIndex + 2} / ${pair.bIndex + 2}`,
      '類似議題の可能性',
      `${pair.a.topic} / ${pair.b.topic}`,
      `${pair.a.category} / ${pair.b.category}`,
      `${pair.a.owner} / ${pair.b.owner}`,
      ''
    ]);
  });

  const issueSheet = getOrCreateSheet_(ss, CONFIG.issueSheetName);
  issueSheet.clear();
  issueSheet.getRange(1, 1, 1, 6).setValues([[
    '回答行',
    '確認種別',
    '議題',
    'カテゴリ',
    '担当',
    '希望扱い'
  ]]);

  if (issues.length === 0) {
    issueSheet.getRange(2, 1).setValue('確認が必要な行はありません。');
    return;
  }

  issueSheet.getRange(2, 1, issues.length, issues[0].length).setValues(issues);
  issueSheet.autoResizeColumns(1, 6);
}

function findSimilarTopicPairs_(rows) {
  const pairs = [];

  for (let i = 0; i < rows.length; i++) {
    for (let j = i + 1; j < rows.length; j++) {
      const score = similarityScore_(rows[i].topic, rows[j].topic);
      if (score >= 0.6) {
        pairs.push({ aIndex: i, bIndex: j, a: rows[i], b: rows[j], score });
      }
    }
  }

  return pairs;
}

function similarityScore_(a, b) {
  const termsA = splitTerms_(a);
  const termsB = splitTerms_(b);
  if (termsA.size === 0 || termsB.size === 0) return 0;

  const intersection = new Set([...termsA].filter(term => termsB.has(term)));
  const union = new Set([...termsA, ...termsB]);
  return intersection.size / union.size;
}

function splitTerms_(text) {
  return new Set(
    String(text)
      .toLowerCase()
      .replace(/[、。,.()()「」『』【】\[\]\s]/g, ' ')
      .split(' ')
      .map(term => term.trim())
      .filter(term => term.length >= 2)
  );
}

類似議題の判定は、厳密な自然言語処理ではありません。

ただ、職員会議の議題募集では、完全一致よりも「同じ話かもしれないものを事前に見つける」ことが重要です。

そのため、ここでは単語集合の重なりで、確認候補を出すだけにしています。

4. 確認メール文面を作る

送信まで自動化すると、宛先ミスや内容未確認のリスクが上がります。

まずは、確認用のメール文面だけをシートに出す形にします。

function buildConfirmationText() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const agendaSheet = ss.getSheetByName(CONFIG.agendaSheetName);
  if (!agendaSheet) {
    throw new Error(`先に ${CONFIG.agendaSheetName} を作成してください`);
  }

  const values = agendaSheet.getDataRange().getValues();
  const rows = values.slice(1).filter(row => row[2]);

  const lines = [
    '職員会議の議題案を以下の通り整理しました。',
    '',
    ...rows.map(row => `${row[0]}. [${row[1]}] ${row[2]}(担当: ${row[3] || '未入力'})`),
    '',
    '担当未入力や重複の可能性があるものは、確認事項シートを参照してください。'
  ];

  const sheet = getOrCreateSheet_(ss, 'Mail Draft');
  sheet.clear();
  sheet.getRange(1, 1).setValue(lines.join('\n'));
  sheet.autoResizeColumn(1);
}

ここでは MailApp.sendEmail() を使っていません。

本文を人が確認してから通常のメール作成画面に貼り付ける運用でも、会議前の整形作業はかなり減らせます。

検証

次のようなサンプル回答を入れて、動作を確認します。

タイムスタンプ 議題 カテゴリ 担当 希望扱い 補足
2026/09/01 09:00 文化祭準備の役割分担 相談 A係 協議 当日係を確認したい
2026/09/01 10:00 文化祭の係分担について 相談 協議 分担表の確認
2026/09/01 11:00 月末提出物の確認 報告 B係 共有 今月分
2026/09/02 09:00 備品購入の承認 決裁 C係 決裁 見積確認

期待する結果は次の通りです。

  • Agenda では、決裁相談報告 の順に並ぶ
  • 担当が空欄の「文化祭の係分担について」が Issues に出る
  • 期限後の「備品購入の承認」が Issues に出る
  • 文化祭に関する2件が、類似議題の候補として Issues に出る

最低限、次の観点で見ます。

  • 元のフォーム回答シートが変更されていない
  • Agenda の番号が1から振り直されている
  • Issues に確認理由が残っている
  • メール文面に担当未入力の表示が残る

再発防止

一度作って終わりにすると、次の会議でまた手作業に戻りがちです。

再発防止として、次の3つを運用に入れます。

1. フォーム入力を選択式に寄せる

カテゴリや希望扱いを自由記述にすると、相談協議話し合い のように表記が揺れます。

フォーム側で選択式にしておくと、GAS側の条件分岐が単純になります。

2. 回答シートを直接編集しない

回答シートを手で整えると、次回のフォーム回答が追加されたときに並びや書式が崩れます。

生データは残し、会議用シートは毎回作り直すほうが扱いやすいです。

3. 自動送信は最後にする

最初から送信まで自動化すると、誤ったアジェンダをそのまま配信するリスクがあります。

先に AgendaIssues を作り、確認すべき点が見える状態にしてから、必要に応じて送信処理を追加するのが現実的です。

まとめ

職員会議の議題募集は、フォームを作るだけでは終わりません。

会議で使うには、カテゴリ順に並べ、担当未入力や期限後投稿を見つけ、似た議題をまとめる必要があります。

GASで回答ログと会議アジェンダを分けておくと、フォーム回答を壊さず、会議前の整形作業を小さくできます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?