1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Google Apps Scriptで「毎朝の欠席連絡まとめ」を自動生成する

1
Posted at

この記事で作るもの

Googleフォームで集めた欠席・遅刻・早退連絡を、Googleスプレッドシート上で毎朝クラス別に集計し、職員室で確認しやすい「本日の連絡一覧」シートへ自動転記する仕組みです。

対象は、学校・塾・研修運営などで Google Workspace を使っている教員ICT担当者です。外部APIやAI APIキーは使わず、Google Apps Script だけで動く構成にします。

根拠にする仕様

Google公式ドキュメントには、Apps Script のトリガーについて次の説明があります。

Triggers let Google Apps Script run a function automatically when a certain event occurs.

つまり、毎朝決まった時刻に集計関数を動かす用途には、時間主導型トリガーが向いています。

また、Properties Service については次の説明があります。

The Properties service stores data in key-value pairs scoped to one script

この記事では「前回処理した行番号」を Script Properties に保存し、同じ連絡を二重転記しないようにします。

さらに、Lock Service については次の説明があります。

prevent simultaneous access to sections of code

時間トリガーの再実行や手動実行が重なった場合に備え、集計処理の先頭でロックを取ります。

参考:

シート構成

Googleフォームの回答先スプレッドシートに、次の2シートがある前提です。

フォームの回答 1

フォーム回答が入る標準シートです。列は次のようにします。

項目
A タイムスタンプ
B 学年
C
D 出席番号
E 氏名
F 種別
G 理由
H 保護者確認

本日の連絡一覧

Apps Script が自動で出力するシートです。存在しない場合はコード側で作成します。

項目
A 受付時刻
B クラス
C 出席番号
D 氏名
E 種別
F 理由
G 保護者確認

コード

スプレッドシートで「拡張機能」から Apps Script を開き、次のコードを貼り付けます。

const CONFIG = {
  responseSheetName: 'フォームの回答 1',
  outputSheetName: '本日の連絡一覧',
  lastProcessedRowKey: 'LAST_PROCESSED_ROW',
  headerRows: 1,
};

function summarizeTodayAttendanceContacts() {
  const lock = LockService.getScriptLock();
  const locked = lock.tryLock(30 * 1000);

  if (!locked) {
    console.warn('別の実行が処理中のためスキップしました。');
    return;
  }

  try {
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    const responseSheet = spreadsheet.getSheetByName(CONFIG.responseSheetName);

    if (!responseSheet) {
      throw new Error(`回答シートが見つかりません: ${CONFIG.responseSheetName}`);
    }

    const outputSheet = getOrCreateOutputSheet_(spreadsheet);
    const properties = PropertiesService.getScriptProperties();
    const lastProcessedRow = Number(
      properties.getProperty(CONFIG.lastProcessedRowKey) || CONFIG.headerRows,
    );

    const lastRow = responseSheet.getLastRow();
    if (lastRow <= lastProcessedRow) {
      return;
    }

    const values = responseSheet
      .getRange(lastProcessedRow + 1, 1, lastRow - lastProcessedRow, 8)
      .getValues();

    const todayRows = values
      .filter(isTodayContact_)
      .map(toOutputRow_);

    if (todayRows.length > 0) {
      outputSheet
        .getRange(outputSheet.getLastRow() + 1, 1, todayRows.length, todayRows[0].length)
        .setValues(todayRows);
    }

    properties.setProperty(CONFIG.lastProcessedRowKey, String(lastRow));
  } finally {
    lock.releaseLock();
  }
}

function resetTodayAttendanceOutput() {
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  const outputSheet = getOrCreateOutputSheet_(spreadsheet);

  outputSheet.clearContents();
  outputSheet.getRange(1, 1, 1, 7).setValues([[
    '受付時刻',
    'クラス',
    '出席番号',
    '氏名',
    '種別',
    '理由',
    '保護者確認',
  ]]);
  outputSheet.setFrozenRows(1);
}

function createMorningTrigger() {
  ScriptApp.newTrigger('summarizeTodayAttendanceContacts')
    .timeBased()
    .everyDays(1)
    .atHour(8)
    .create();
}

function getOrCreateOutputSheet_(spreadsheet) {
  const existingSheet = spreadsheet.getSheetByName(CONFIG.outputSheetName);
  if (existingSheet) {
    return existingSheet;
  }

  const sheet = spreadsheet.insertSheet(CONFIG.outputSheetName);
  sheet.getRange(1, 1, 1, 7).setValues([[
    '受付時刻',
    'クラス',
    '出席番号',
    '氏名',
    '種別',
    '理由',
    '保護者確認',
  ]]);
  sheet.setFrozenRows(1);
  return sheet;
}

function isTodayContact_(row) {
  const timestamp = row[0];
  if (!(timestamp instanceof Date)) {
    return false;
  }

  const timezone = Session.getScriptTimeZone();
  const today = Utilities.formatDate(new Date(), timezone, 'yyyy-MM-dd');
  const targetDate = Utilities.formatDate(timestamp, timezone, 'yyyy-MM-dd');

  return today === targetDate;
}

function toOutputRow_(row) {
  const timestamp = row[0];
  const grade = row[1];
  const className = row[2];
  const studentNumber = row[3];
  const studentName = row[4];
  const contactType = row[5];
  const reason = row[6];
  const guardianConfirmed = row[7];

  return [
    timestamp,
    `${grade}-${className}`,
    studentNumber,
    studentName,
    contactType,
    reason,
    guardianConfirmed,
  ];
}

初回セットアップ

  1. フォーム回答先のスプレッドシートを開く
  2. 「拡張機能」から Apps Script を開く
  3. 上のコードを貼り付ける
  4. resetTodayAttendanceOutput を手動実行する
  5. summarizeTodayAttendanceContacts を手動実行し、権限を承認する
  6. createMorningTrigger を1回だけ実行する

createMorningTrigger は毎日8時台に集計関数を動かすトリガーを作ります。学校の運用に合わせて、atHour(8)atHour(7) などに変更してください。

運用時の注意

1. APIキーやパスワードをコードに書かない

この記事のコードは外部APIを使わないため、APIキーは不要です。もし将来、AI要約や外部通知サービスを追加する場合でも、ブラウザ側JavaScriptからAI APIを直接呼ぶ構成にはしないでください。

校務データは個人情報を含みます。APIキー、トークン、パスワード、児童生徒の個人情報をリポジトリへコミットしない運用にしてください。

2. 二重転記を防ぐ

LAST_PROCESSED_ROW に前回処理した最終行を保存しているため、同じフォーム回答を何度も転記しにくい構成です。加えて LockService.getScriptLock() で同時実行を避けています。

ただし、フォーム回答シートの途中行を削除したり並べ替えたりすると、保存済みの行番号と実データの対応が崩れます。回答シートは原本として扱い、加工は別シートで行うのが安全です。

3. 上限を意識する

Google公式の Quotas ページには、Apps Script のサービスには日次クォータや実行時間の制限があると説明されています。欠席連絡の集計程度なら通常は軽い処理ですが、全校規模で複数フォームを連携する場合は、毎回全行スキャンする設計を避け、今回のように「前回処理した行から読む」形にしておくと安定します。

少し改善するなら

現場で使うなら、次の改善が実用的です。

  • 種別が「発熱」「感染症疑い」の場合だけ背景色を変える
  • 学年・組ごとに並べ替えて出力する
  • 保護者確認が空欄の回答だけ別シートに抽出する
  • 朝の集計後に担当者へメール通知する

メール通知まで入れる場合は、送信先の誤設定が事故につながります。まずはスプレッドシート内で一覧化し、運用が固まってから通知を追加する方が無難です。

まとめ

Googleフォームと Apps Script を組み合わせると、毎朝の欠席・遅刻・早退連絡を手作業で転記する時間を減らせます。

今回のポイントは次の3つです。

  • 時間主導型トリガーで毎朝自動実行する
  • Script Properties で前回処理した行を保存する
  • Lock Service で同時実行による二重処理を避ける

小さな自動化でも、朝の確認作業の抜け漏れを減らせます。最初は1フォーム・1シートの小さい範囲から始め、個人情報の扱いと権限設定を確認しながら広げるのがよいです。

1
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?