完了したはずのタスクが期限切れになる前に|GASで状態と期限をdry-run監査

完了したはずのタスクが期限切れになる前に|GASで状態と期限をdry-run監査

ツールおじ|GoogleアカウントをAIエージェント化

ツールおじ|GoogleアカウントをAIエージェント化

タスク表では、「完了」なのに完了日が空欄、未完了なのに期限を過ぎている、といった小さな矛盾が後から集計を崩します。この記事では、元シートを書き換えず、タスクID・状態・期限・完了日の組み合わせだけを点検する無料のGASを紹介します。

実行結果は件数と行番号だけをログへ出します。タスク名やメモ本文はログへ出さず、初期設定では監査結果シートも作りません。

この無料コードで確認できること

  • タスクIDの空欄と重複
  • 状態が「未着手・進行中・保留・完了」以外になっている行
  • 未完了のまま期限を過ぎた行
  • 完了なのに完了日が空欄の行
  • 未完了なのに完了日が入っている行
  • 完了日が期限より前後どちらにあるかを確認すべき行
  • 優先度が「高・中・低」以外になっている行

期限切れタスクを自動で完了にしたり、セルを削除したりはしません。まず確認候補だけを洗い出すためのdry-runです。

先に知っておきたいこと

  • 対象はスプレッドシートの1枚のタスク表です。
  • 日付はスプレッドシート上で日付として認識される値、または `yyyy-MM-dd` 形式を想定します。
  • コードはメール送信、外部通知、トリガー作成、行削除を行いません。
  • タスク名、担当者、メモなどの内容はログへ出しません。
  • 監査結果シートへの書き出しは初期状態でOFFです。
  • 実データへ適用する前に、必ずコピーしたシートで少量のサンプルを使ってください。

準備するシート

シート名を `Tasks` にし、1行目へ次の列を用意します。

  • タスクID
  • タスク名
  • 状態
  • 期限
  • 完了日
  • 優先度

状態は `未着手`、`進行中`、`保留`、`完了` のいずれか、優先度は `高`、`中`、`低` のいずれかにそろえます。

サンプルとして、次のような行で試せます。

  • T-001 / 見積書確認 / 進行中 / 2026-10-05 / 空欄 / 高
  • T-002 / 公開前チェック / 完了 / 2026-10-06 / 2026-10-06 / 中
  • T-003 / 画像差し替え / 完了 / 2026-10-07 / 空欄 / 低

3行目は「完了なのに完了日が空欄」として候補になります。

コピーして使える最小コード

function auditTaskStatusAndDeadlineFree() {
  const config = {
    sheetName: 'Tasks',
    headerRow: 1,
    maxRows: 500,
    dryRun: true,
    writeReport: false,
    reportSheetName: 'TaskAuditReport'
  };

  validateTaskAuditConfig_(config);
  if (config.dryRun !== true) {
    throw new Error('安全のため dryRun: true のまま実行してください。');
  }

  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const source = ss.getSheetByName(config.sheetName);
  if (!source) throw new Error('対象シートが見つかりません: ' + config.sheetName);

  const lastRow = source.getLastRow();
  const lastColumn = source.getLastColumn();
  if (lastRow <= config.headerRow) {
    throw new Error('確認するデータ行がありません。');
  }
  if (lastRow - config.headerRow > config.maxRows) {
    throw new Error('対象が上限を超えています。maxRows を確認してください。');
  }

  const values = source
    .getRange(config.headerRow, 1, lastRow - config.headerRow + 1, lastColumn)
    .getValues();
  const headers = values[0].map(value => String(value).trim());
  const required = ['タスクID', 'タスク名', '状態', '期限', '完了日', '優先度'];
  const index = {};
  required.forEach(name => {
    const column = headers.indexOf(name);
    if (column === -1) throw new Error('必須列がありません: ' + name);
    index[name] = column;
  });

  const allowedStatuses = new Set(['未着手', '進行中', '保留', '完了']);
  const allowedPriorities = new Set(['高', '中', '低']);
  const seenIds = new Map();
  const issues = [];
  const today = startOfDay_(new Date());

  values.slice(1).forEach((row, offset) => {
    const rowNumber = config.headerRow + offset + 1;
    const taskId = String(row[index['タスクID']] ?? '').trim();
    const taskName = String(row[index['タスク名']] ?? '').trim();
    const status = String(row[index['状態']] ?? '').trim();
    const priority = String(row[index['優先度']] ?? '').trim();
    const dueDate = parseTaskDate_(row[index['期限']]);
    const completedDate = parseTaskDate_(row[index['完了日']]);

    if (!taskId && !taskName && !status && !dueDate && !completedDate && !priority) return;

    if (!taskId) {
      addTaskIssue_(issues, rowNumber, 'ERROR', 'タスクIDが空欄');
    } else if (seenIds.has(taskId)) {
      addTaskIssue_(issues, rowNumber, 'ERROR', 'タスクIDが重複', '先頭行: ' + seenIds.get(taskId));
    } else {
      seenIds.set(taskId, rowNumber);
    }

    if (!taskName) addTaskIssue_(issues, rowNumber, 'ERROR', 'タスク名が空欄');
    if (!allowedStatuses.has(status)) {
      addTaskIssue_(issues, rowNumber, 'ERROR', '状態が候補外');
    }
    if (!allowedPriorities.has(priority)) {
      addTaskIssue_(issues, rowNumber, 'WARN', '優先度が候補外');
    }

    const rawDueDate = row[index['期限']];
    const rawCompletedDate = row[index['完了日']];
    if (rawDueDate !== '' && !dueDate) {
      addTaskIssue_(issues, rowNumber, 'ERROR', '期限の日付形式を確認');
    }
    if (rawCompletedDate !== '' && !completedDate) {
      addTaskIssue_(issues, rowNumber, 'ERROR', '完了日の日付形式を確認');
    }

    if (status !== '完了' && dueDate && startOfDay_(dueDate) < today) {
      addTaskIssue_(issues, rowNumber, 'WARN', '未完了のまま期限超過');
    }
    if (status === '完了' && !completedDate) {
      addTaskIssue_(issues, rowNumber, 'WARN', '完了なのに完了日が空欄');
    }
    if (status !== '完了' && completedDate) {
      addTaskIssue_(issues, rowNumber, 'WARN', '未完了なのに完了日あり');
    }
    if (dueDate && completedDate && startOfDay_(completedDate) < startOfDay_(dueDate)) {
      addTaskIssue_(issues, rowNumber, 'INFO', '期限より前に完了。意図を確認');
    }
  });

  const summary = issues.reduce((result, issue) => {
    result[issue.level] = (result[issue.level] || 0) + 1;
    return result;
  }, {});
  console.log(JSON.stringify({
    checkedRows: values.length - 1,
    issueCount: issues.length,
    summary,
    issueRows: [...new Set(issues.map(issue => issue.rowNumber))],
    note: 'タスク名・担当者・メモ本文はログへ出していません。'
  }));

  if (config.writeReport === true) {
    writeTaskAuditReport_(ss, config.reportSheetName, issues);
  }
}

function validateTaskAuditConfig_(config) {
  if (!config || typeof config !== 'object') throw new Error('設定が不正です。');
  if (!config.sheetName) throw new Error('sheetName を指定してください。');
  if (!Number.isInteger(config.headerRow) || config.headerRow < 1) {
    throw new Error('headerRow は1以上の整数にしてください。');
  }
  if (!Number.isInteger(config.maxRows) || config.maxRows < 1 || config.maxRows > 5000) {
    throw new Error('maxRows は1〜5000にしてください。');
  }
  if (config.dryRun !== true) throw new Error('dryRun は true 固定です。');
}

function parseTaskDate_(value) {
  if (value === '' || value == null) return null;
  if (value instanceof Date && !Number.isNaN(value.getTime())) return value;
  const text = String(value).trim();
  if (!/^\d{4}-\d{2}-\d{2}$/.test(text)) return null;
  const date = new Date(text + 'T00:00:00');
  return Number.isNaN(date.getTime()) ? null : date;
}

function startOfDay_(date) {
  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}

function addTaskIssue_(issues, rowNumber, level, reason, detail) {
  issues.push({ rowNumber, level, reason, detail: detail || '' });
}

function writeTaskAuditReport_(ss, sheetName, issues) {
  let report = ss.getSheetByName(sheetName);
  if (!report) report = ss.insertSheet(sheetName);
  report.clearContents();
  const rows = [['行番号', '重要度', '確認内容', '補足']]
    .concat(issues.map(issue => [issue.rowNumber, issue.level, issue.reason, issue.detail]));
  report.getRange(1, 1, rows.length, rows[0].length).setValues(rows);
}

実行手順

  1. 元のスプレッドシートをコピーします。
  2. コピー側で `Tasks` シートと6列の見出しを用意します。
  3. 拡張機能からApps Scriptを開き、コードを貼り付けます。
  4. `auditTaskStatusAndDeadlineFree` を選び、少量のサンプルで実行します。
  5. 実行ログで確認件数、重要度別件数、候補行番号を確認します。
  6. 候補行を目視し、状態や日付を人が判断して修正します。

最初は `writeReport: false` のまま使ってください。結果シートが必要な場合だけ、コピー環境で内容を確認してから `true` にします。

ログの見方

ログには次の情報だけを出します。

  • 確認した行数
  • 問題候補の件数
  • ERROR、WARN、INFOの件数
  • 問題候補の行番号

タスク名、担当者、メモ本文は出しません。共有ログや画面録画へ業務内容が残りにくい形にしています。

失敗しやすい点と制限

  • 見出しの全角・半角、余分な空白があると必須列を認識できません。
  • `2026/10/6` のような文字列は環境差が出やすいため、文字入力なら `2026-10-06` を使ってください。
  • 期限超過の判定はスクリプト実行日のタイムゾーンに影響されます。スプレッドシートとApps Scriptのタイムゾーンをそろえてください。
  • 休日、営業日、担当者ごとのルール、依存関係は判定しません。
  • 「期限より前に完了」は異常とは限らないためINFOです。意図した前倒し完了なら修正不要です。
  • 500行を初期上限にしています。いきなり上限を増やさず、実行時間とログ量を確認してください。
  • 結果シートを有効にすると、そのシートの内容は毎回上書きされます。元データの `Tasks` シートは変更しません。

実務での確認チェックリスト

  • [ ] コピーしたスプレッドシートで試した
  • [ ] 必須列名が完全一致している
  • [ ] `dryRun: true` を維持している
  • [ ] `writeReport: false` で最初の結果を見た
  • [ ] 候補行を自動修正せず、人が内容を確認した
  • [ ] タスク名や個人情報をログへ追加していない
  • [ ] タイムゾーンを確認した

まとめ

タスク表の不整合は、件数集計や朝の優先順位づけを静かに狂わせます。最初から自動更新するのではなく、状態・期限・完了日の矛盾を行番号だけでdry-run確認してから直すと、安全に運用を整えられます。

朝のタスク整理と確認を仕組みにしたい方はこちら

GasおじのTips商品一覧を見る

※この記事は学習・検証用の無料例です。実データへ適用する前にコピー環境で確認してください。Google公式・Google公認の商品ではありません。


あなたも記事の投稿・販売を
始めてみませんか?

Tipsなら簡単に記事を販売できます!
登録無料で始められます!

Tipsなら、無料ですぐに記事の販売をはじめることができます Tipsの詳細はこちら
 

この記事の販売者

ツールおじ|GoogleアカウントをAIエージェント化

おう、Gasおじだ。 AIツールとSaaSに散々突っ込んで遠回りした結果、最後に強かったのは「今あるGoogleアカウント」だった。 今ではAIエージェントであらゆるタスクを自動化して無双してるぞ。 GAS、スプレッドシート、フォーム、Gmail、AIエージェントを組み合わせれば、個人でも小さな会社でも、手作業だらけの業務はかなり自動化できる。 ここでは机上のAI論じゃなく、現場で動くテンプレ、コード、手順だけを置いていくぞ。 高いツールを増やす前に、まずお前さんのGoogleアカウントを仕事するエージェントに変えようじゃないか。

この販売者が書いた他の記事

  • Gmailの予約送信ミスを送信前に発見|GASで宛先・日時をdry-run監査

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    1 %獲得
    (29 円相当)
  • スプレッドシートの入力ミスを公開前に発見|GASでルール表をdry-run監査

関連のおすすめ記事

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    1 %獲得
    (29 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

    ツールおじ|GoogleアカウントをAIエージェント化

  • USBメモリで持ち運べるLinux Mint 22.1と永続的な書き込み領域の作成法【Rufus だけで作成する方法を初心者向けに徹底解説】

    ¥500
    1 %獲得
    (5 円相当)
    SmartSenior

    SmartSenior

  • 毎朝のToDoを区分・優先度・期限つきでまとめる Morning ToDo Mail Pro

    ¥2,980
    1 %獲得
    (29 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

    ツールおじ|GoogleアカウントをAIエージェント化