完了したはずのタスクが期限切れになる前に|GASで状態と期限をdry-run監査
ツールおじ|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);
}実行手順
- 元のスプレッドシートをコピーします。
- コピー側で `Tasks` シートと6列の見出しを用意します。
- 拡張機能からApps Scriptを開き、コードを貼り付けます。
- `auditTaskStatusAndDeadlineFree` を選び、少量のサンプルで実行します。
- 実行ログで確認件数、重要度別件数、候補行番号を確認します。
- 候補行を目視し、状態や日付を人が判断して修正します。
最初は `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公認の商品ではありません。
