はじめに
毎日の日報作成と管理、面倒ではありませんか?手動でメール送信、PDF保存、データ管理...これらの作業を自動化できたら業務効率が大幅に向上しますよね。
今回は**Google Apps Script(GAS)**を使って、以下の機能を持つ本格的な日報管理システムを作成します:
- 📝 Webフォームでの日報入力
- 📧 上司への自動メール送信
- 📊 スプレッドシートでのデータ管理
- 📄 PDF自動生成・Drive保存
- ✅ メール送信状況の追跡
完成イメージ
システム構成
日報入力フォーム → GAS処理 → スプレッドシート保存
↓
メール送信 + PDF生成
主な特徴
- ゼロコスト: Google Workspaceの無料機能のみ使用
- 自動化: 手動作業を最小限に
- 追跡可能: メール送信状況を記録
- 拡張性: テンプレート機能で様々な業務に対応
システム構築手順
1. スプレッドシートの作成
- Google スプレッドシートで新しいシートを作成
- 「拡張機能」→「Apps Script」を選択
- プロジェクト名を「日報管理システム」に変更
2. メインコードの実装
以下のコードをCode.gs
に貼り付けます:
// ==================================================
// 日報管理システム - メインGASコード (日報.gs)
// ==================================================
/**
* スプレッドシートを開いたときのトリガー - カスタムメニューの追加
*/
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu("📋 日報メニュー")
.addItem("📄 パネルを開く", "showReportPanel")
.addItem("📝 日報を入力", "showInputForm")
.addItem("⚙️ システム初期化", "initializeSheet")
.addToUi();
ui.alert("ようこそ!", "日報を入力するには「📋 日報メニュー」から開始してください。", ui.ButtonSet.OK);
}
/**
* セルクリック時のトリガー(A1セルクリックで日報フォーム表示)
*/
function onEdit(e) {
const range = e.range;
// A1セル(日報入力ボタン)がクリックされた場合
if (range.getA1Notation() === "A1") {
showInputForm();
return;
}
}
// ==================================================
// 画面表示関数
// ==================================================
/**
* 日報フォームをモーダルで表示(HTML: reportForm.html を使用)
*/
function showInputForm() {
const html = HtmlService.createHtmlOutputFromFile('reportForm')
.setWidth(1000)
.setHeight(1800);
SpreadsheetApp.getUi().showModalDialog(html, '日報入力システム');
}
// ==================================================
// システム初期化関数
// ==================================================
/**
* システム初期化 - スプレッドシートとシートを設定
*/
function initializeSheet() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const newTitle = "日報管理システム";
const newSheetName = "日報データ";
// スプレッドシートのタイトルを変更
spreadsheet.rename(newTitle);
// アクティブシートを取得してクリア
let sheet = spreadsheet.getActiveSheet();
sheet.clear();
// シート名を変更
try {
sheet.setName(newSheetName);
} catch (e) {
Logger.log("シート名変更スキップ: 既に同名のシートが存在");
}
// ヘッダー作成(メール送信状況列を含む)
const headers = [
"提出日時", "氏名", "所属部署", "業務内容",
"成果・進捗", "課題・問題点", "明日の予定", "備考", "メール送信状況"
];
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
// ヘッダーの装飾
const headerRange = sheet.getRange(1, 1, 1, headers.length);
headerRange.setFontWeight("bold");
headerRange.setBackground("#34a853");
headerRange.setFontColor("#ffffff");
headerRange.setHorizontalAlignment("center");
// 列幅自動調整
sheet.autoResizeColumns(1, headers.length);
Logger.log("✅ 日報管理システムの初期化完了");
}
// ==================================================
// 日報データ保存関数
// ==================================================
/**
* 日報データをスプレッドシートに保存(メイン関数)
*/
function submitReport(reportData) {
try {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("日報データ");
// データの検証
if (!reportData.name || !reportData.department || !reportData.workContent) {
throw new Error("必須項目(氏名、所属部署、業務内容)が入力されていません");
}
// メール送信処理
let emailStatus = "未送信";
let emailResult = { success: false, message: "メール送信なし" };
if (reportData.supervisorEmail && reportData.supervisorEmail.trim() !== '') {
emailResult = sendReportEmail(reportData);
if (emailResult.success) {
emailStatus = `送信完了(${Utilities.formatDate(new Date(), "JST", "HH:mm")})`;
} else {
emailStatus = `送信失敗: ${emailResult.message}`;
}
} else {
emailStatus = "メールアドレス未入力";
Logger.log("ℹ️ 上司のメールアドレス未入力のため、メール送信をスキップ");
}
// スプレッドシートに保存するデータ(メール送信状況を含む)
const rowData = [
new Date(), // 提出日時
reportData.name,
reportData.department,
reportData.workContent,
reportData.achievements || '',
reportData.issues || '',
reportData.tomorrowPlan || '',
reportData.remarks || '',
emailStatus // メール送信状況
];
// データを追加
sheet.appendRow(rowData);
Logger.log("✅ スプレッドシートへの保存完了");
// メール送信状況セルの色付け
const lastRow = sheet.getLastRow();
const emailStatusCell = sheet.getRange(lastRow, 9); // I列(メール送信状況)
if (emailResult.success) {
emailStatusCell.setBackground("#d9ead3"); // 薄緑(送信成功)
emailStatusCell.setFontColor("#274e13");
} else if (emailStatus.includes("失敗")) {
emailStatusCell.setBackground("#f4cccc"); // 薄赤(送信失敗)
emailStatusCell.setFontColor("#cc0000");
} else {
emailStatusCell.setBackground("#fff2cc"); // 薄黄(未送信・未入力)
emailStatusCell.setFontColor("#bf9000");
}
// PDF生成
let pdfUrl = '';
try {
const pdfBlob = generatePDF(reportData);
pdfUrl = savePDFToDrive(pdfBlob, reportData.name);
Logger.log("✅ PDF生成・保存完了: " + pdfUrl);
} catch (pdfError) {
Logger.log("⚠️ PDF生成エラー: " + pdfError.toString());
}
return {
success: true,
message: `日報の提出が完了しました。メール送信: ${emailStatus}`,
pdfUrl: pdfUrl,
emailSent: emailResult.success,
emailStatus: emailStatus
};
} catch (error) {
Logger.log("❌ submitReportエラー: " + error.toString());
return {
success: false,
message: "エラーが発生しました: " + error.toString()
};
}
}
// ==================================================
// メール送信関数
// ==================================================
/**
* 上司にメール送信
*/
function sendReportEmail(reportData) {
try {
// 入力データの検証
if (!reportData.supervisorEmail || reportData.supervisorEmail.trim() === '') {
Logger.log("❌ メール送信スキップ: 上司のメールアドレスが未入力");
return { success: false, message: "上司のメールアドレスが未入力です" };
}
// メールアドレスの形式チェック
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(reportData.supervisorEmail)) {
Logger.log("❌ メール送信失敗: 無効なメールアドレス形式 - " + reportData.supervisorEmail);
return { success: false, message: "メールアドレスの形式が正しくありません" };
}
const currentDate = new Date();
const subject = `【日報】${reportData.name} - ${Utilities.formatDate(currentDate, "JST", "yyyy/MM/dd")}`;
const body = `
${reportData.name}さんの日報をお送りします。
■ 基本情報
・氏名: ${reportData.name}
・所属部署: ${reportData.department}
・提出日: ${Utilities.formatDate(currentDate, "JST", "yyyy年MM月dd日")}
■ 業務内容
${reportData.workContent || '(未入力)'}
■ 成果・進捗
${reportData.achievements || '(未入力)'}
■ 課題・問題点
${reportData.issues || '(未入力)'}
■ 明日の予定
${reportData.tomorrowPlan || '(未入力)'}
■ 備考
${reportData.remarks || '(未入力)'}
--
このメールは日報管理システムから自動送信されています。
送信日時: ${Utilities.formatDate(currentDate, "JST", "yyyy年MM月dd日 HH:mm:ss")}
`;
Logger.log("📧 メール送信開始");
Logger.log("宛先: " + reportData.supervisorEmail);
Logger.log("件名: " + subject);
// メール送信実行
MailApp.sendEmail({
to: reportData.supervisorEmail,
subject: subject,
body: body
});
Logger.log("✅ メール送信完了: " + reportData.supervisorEmail);
return { success: true, message: "メール送信完了" };
} catch (error) {
Logger.log("❌ メール送信エラー: " + error.toString());
return { success: false, message: "メール送信エラー: " + error.toString() };
}
}
// ==================================================
// PDF生成・保存関数
// ==================================================
/**
* PDF生成
*/
function generatePDF(reportData) {
const doc = DocumentApp.create(`日報_${reportData.name}_${Utilities.formatDate(new Date(), "JST", "yyyyMMdd")}`);
const body = doc.getBody();
body.appendParagraph("日報").setHeading(DocumentApp.ParagraphHeading.TITLE);
body.appendParagraph("");
const table = [
["提出日", Utilities.formatDate(new Date(), "JST", "yyyy年MM月dd日")],
["氏名", reportData.name],
["所属部署", reportData.department],
["業務内容", reportData.workContent],
["成果・進捗", reportData.achievements],
["課題・問題点", reportData.issues],
["明日の予定", reportData.tomorrowPlan],
["備考", reportData.remarks]
];
const docTable = body.appendTable(table);
docTable.setColumnWidth(0, 100);
docTable.setColumnWidth(1, 400);
// ドキュメントの変更を保存して閉じる
doc.saveAndClose();
// PDFに変換
const pdfBlob = DriveApp.getFileById(doc.getId()).getBlob().getAs('application/pdf');
// 一時ドキュメントを削除
DriveApp.getFileById(doc.getId()).setTrashed(true);
return pdfBlob;
}
/**
* PDFをDriveに保存
*/
function savePDFToDrive(pdfBlob, employeeName) {
const folderName = "日報PDF";
let folder;
try {
folder = DriveApp.getFoldersByName(folderName).next();
} catch (e) {
folder = DriveApp.createFolder(folderName);
}
const fileName = `日報_${employeeName}_${Utilities.formatDate(new Date(), "JST", "yyyyMMdd")}.pdf`;
const file = folder.createFile(pdfBlob.setName(fileName));
return file.getUrl();
}
// ==================================================
// テンプレート・ユーティリティ関数
// ==================================================
/**
* 日報テンプレートを取得
*/
function getReportTemplates() {
return {
development: {
workContent: "システム開発業務(要件定義、設計、プログラミング、テスト)",
achievements: "・機能Aの実装完了\n・単体テスト実施\n・コードレビュー対応",
issues: "・データベース接続でパフォーマンス課題あり\n・API仕様の詳細確認が必要",
tomorrowPlan: "・機能Bの実装開始\n・パフォーマンス改善の調査\n・チームミーティング参加"
},
maintenance: {
workContent: "システム保守・運用業務",
achievements: "・障害対応完了\n・定期メンテナンス実施\n・監視システムの確認",
issues: "・サーバー負荷が高い時間帯がある\n・ログの保存期間見直しが必要",
tomorrowPlan: "・負荷分散の検討\n・ログ管理ポリシーの確認\n・月次レポート作成"
},
training: {
workContent: "研修・学習業務",
achievements: "・技術研修受講完了\n・新技術の調査実施\n・実習課題の完成",
issues: "・理解が不足している部分がある\n・実際の業務への適用方法が不明",
tomorrowPlan: "・復習と追加学習\n・先輩社員への質問\n・実践的な課題への取り組み"
},
meeting: {
workContent: "会議・打ち合わせ業務",
achievements: "・プロジェクト進捗会議参加\n・要件整理と課題の明確化\n・次のアクションプランの策定",
issues: "・スケジュールが厳しい\n・リソース配分の調整が必要",
tomorrowPlan: "・タスクの優先順位見直し\n・関係者との調整\n・進捗状況の報告準備"
}
};
}
// ==================================================
// デバッグ・テスト用関数
// ==================================================
/**
* メール送信テスト用関数
*/
function testEmailSending() {
const testData = {
name: "テスト太郎",
department: "システム開発部",
workContent: "テスト業務",
achievements: "テスト成果",
issues: "テスト課題",
tomorrowPlan: "テスト予定",
remarks: "テスト備考",
supervisorEmail: "test@example.com" // 実際のテスト用メールアドレスに変更
};
console.log("📧 メール送信テスト開始");
const result = sendReportEmail(testData);
console.log("結果:", result);
return result;
}
/**
* Gmail送信制限チェック用関数
*/
function checkEmailQuota() {
try {
const quota = MailApp.getRemainingDailyQuota();
Logger.log("📊 Gmail送信可能残数: " + quota);
if (quota <= 0) {
Logger.log("⚠️ Gmail送信制限に達しています。24時間後に再試行してください。");
return { available: false, remaining: 0 };
}
return { available: true, remaining: quota };
} catch (error) {
Logger.log("❌ Gmail送信制限チェックエラー: " + error.toString());
return { available: false, remaining: 0, error: error.toString() };
}
}
3. HTMLフォームの作成
新しいHTMLファイル(reportForm.html
)を作成します:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>日報入力システム</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 900px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #333;
}
input[type="text"], input[type="email"], textarea, select {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 14px;
transition: border-color 0.3s;
}
input[type="text"]:focus, input[type="email"]:focus, textarea:focus, select:focus {
outline: none;
border-color: #4285f4;
}
textarea {
resize: vertical;
min-height: 100px;
}
.submit-btn {
background-color: #4285f4;
color: white;
padding: 15px 30px;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.submit-btn:hover {
background-color: #3367d6;
}
.template-btn {
background-color: #34a853;
color: white;
padding: 8px 15px;
border: none;
border-radius: 3px;
font-size: 12px;
cursor: pointer;
margin-left: 10px;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.required {
color: red;
}
</style>
</head>
<body>
<div class="container">
<h1>📝 日報入力システム</h1>
<form id="reportForm">
<div class="form-group">
<label for="name">氏名 <span class="required">*</span></label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="department">所属部署 <span class="required">*</span></label>
<input type="text" id="department" name="department" required>
</div>
<div class="form-group">
<label for="supervisorEmail">上司のメールアドレス</label>
<input type="email" id="supervisorEmail" name="supervisorEmail" placeholder="example@company.com">
</div>
<div class="form-group">
<label for="workContent">業務内容 <span class="required">*</span></label>
<button type="button" class="template-btn" onclick="loadTemplate('development')">開発</button>
<button type="button" class="template-btn" onclick="loadTemplate('maintenance')">保守</button>
<button type="button" class="template-btn" onclick="loadTemplate('training')">研修</button>
<button type="button" class="template-btn" onclick="loadTemplate('meeting')">会議</button>
<textarea id="workContent" name="workContent" required></textarea>
</div>
<div class="form-group">
<label for="achievements">成果・進捗</label>
<textarea id="achievements" name="achievements"></textarea>
</div>
<div class="form-group">
<label for="issues">課題・問題点</label>
<textarea id="issues" name="issues"></textarea>
</div>
<div class="form-group">
<label for="tomorrowPlan">明日の予定</label>
<textarea id="tomorrowPlan" name="tomorrowPlan"></textarea>
</div>
<div class="form-group">
<label for="remarks">備考</label>
<textarea id="remarks" name="remarks"></textarea>
</div>
<button type="submit" class="submit-btn">📤 日報を提出</button>
</form>
</div>
<script>
let templates = {};
// テンプレートを読み込み
google.script.run.withSuccessHandler(function(data) {
templates = data;
}).getReportTemplates();
// テンプレート適用
function loadTemplate(type) {
const template = templates[type];
if (template) {
document.getElementById('workContent').value = template.workContent;
document.getElementById('achievements').value = template.achievements;
document.getElementById('issues').value = template.issues;
document.getElementById('tomorrowPlan').value = template.tomorrowPlan;
}
}
// フォーム送信
document.getElementById('reportForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = {
name: document.getElementById('name').value,
department: document.getElementById('department').value,
supervisorEmail: document.getElementById('supervisorEmail').value,
workContent: document.getElementById('workContent').value,
achievements: document.getElementById('achievements').value,
issues: document.getElementById('issues').value,
tomorrowPlan: document.getElementById('tomorrowPlan').value,
remarks: document.getElementById('remarks').value
};
// 送信ボタンを無効化
const submitBtn = e.target.querySelector('.submit-btn');
submitBtn.disabled = true;
submitBtn.textContent = '送信中...';
google.script.run
.withSuccessHandler(function(result) {
if (result.success) {
alert('✅ ' + result.message);
google.script.host.close();
} else {
alert('❌ ' + result.message);
submitBtn.disabled = false;
submitBtn.textContent = '📤 日報を提出';
}
})
.withFailureHandler(function(error) {
alert('❌ エラーが発生しました: ' + error.toString());
submitBtn.disabled = false;
submitBtn.textContent = '📤 日報を提出';
})
.submitReport(formData);
});
</script>
</body>
</html>
使用方法
1. 初回セットアップ
// GASエディタで実行
initializeSheet();
2. 権限の許可
初回実行時にGoogleから権限要求があります:
- Gmail送信権限
- Drive書き込み権限
- Sheets編集権限
3. 日報入力
- スプレッドシートを開く
- 「📋 日報メニュー」→「📝 日報を入力」を選択
- フォームに必要事項を入力
- 「📤 日報を提出」をクリック
主要機能の詳細
メール送信状況の追跡
スプレッドシートの「メール送信状況」列で確認できます:
- 🟢 送信完了(時刻): 緑色表示
- 🔴 送信失敗: 理由: 赤色表示
- 🟡 メールアドレス未入力: 黄色表示
PDF自動生成
- Google Driveに「日報PDF」フォルダを自動作成
- ファイル名:
日報_氏名_YYYYMMDD.pdf
- 一時的なドキュメントは自動削除
テンプレート機能
4種類の業務テンプレートを用意:
- 開発: システム開発業務
- 保守: システム保守・運用業務
- 研修: 研修・学習業務
- 会議: 会議・打ち合わせ業務
トラブルシューティング
Gmail送信制限
// 送信制限を確認
checkEmailQuota();
- 無料アカウント: 1日100通まで
- Google Workspace: 1日1500通まで
メール送信テスト
// テスト送信
testEmailSending(); // メールアドレスを実際のものに変更
デバッグ方法
- GASエディタで「実行」→「実行ログ」を確認
- エラーの詳細情報をログで確認
カスタマイズポイント
メール本文のカスタマイズ
sendReportEmail
関数のbody
部分を編集
PDF レイアウトの変更
generatePDF
関数でテーブル構造や装飾を変更
追加フィールドの作成
-
initializeSheet
のヘッダー配列に追加 - HTMLフォームに入力欄を追加
-
submitReport
のrowData
配列に追加
まとめ
このシステムにより、日報業務の自動化が実現できます:
- ⏰ 時間短縮: 手動作業を大幅削減
- 📊 データ管理: スプレッドシートで一元管理
- 🔍 追跡可能: メール送信状況を記録
- 💰 コスト削減: 無料で運用可能
業務効率化の第一歩として、ぜひお試しください!