1
2

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で作る本格的な日報管理システム【メール送信・PDF生成・スプレッドシート連携】

Posted at

はじめに

毎日の日報作成と管理、面倒ではありませんか?手動でメール送信、PDF保存、データ管理...これらの作業を自動化できたら業務効率が大幅に向上しますよね。

今回は**Google Apps Script(GAS)**を使って、以下の機能を持つ本格的な日報管理システムを作成します:

  • 📝 Webフォームでの日報入力
  • 📧 上司への自動メール送信
  • 📊 スプレッドシートでのデータ管理
  • 📄 PDF自動生成・Drive保存
  • メール送信状況の追跡

完成イメージ

システム構成

日報入力フォーム → GAS処理 → スプレッドシート保存
                     ↓
               メール送信 + PDF生成

主な特徴

  • ゼロコスト: Google Workspaceの無料機能のみ使用
  • 自動化: 手動作業を最小限に
  • 追跡可能: メール送信状況を記録
  • 拡張性: テンプレート機能で様々な業務に対応

システム構築手順

1. スプレッドシートの作成

  1. Google スプレッドシートで新しいシートを作成
  2. 「拡張機能」→「Apps Script」を選択
  3. プロジェクト名を「日報管理システム」に変更

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. 日報入力

  1. スプレッドシートを開く
  2. 「📋 日報メニュー」→「📝 日報を入力」を選択
  3. フォームに必要事項を入力
  4. 「📤 日報を提出」をクリック

主要機能の詳細

メール送信状況の追跡

スプレッドシートの「メール送信状況」列で確認できます:

  • 🟢 送信完了(時刻): 緑色表示
  • 🔴 送信失敗: 理由: 赤色表示
  • 🟡 メールアドレス未入力: 黄色表示

PDF自動生成

  • Google Driveに「日報PDF」フォルダを自動作成
  • ファイル名: 日報_氏名_YYYYMMDD.pdf
  • 一時的なドキュメントは自動削除

テンプレート機能

4種類の業務テンプレートを用意:

  • 開発: システム開発業務
  • 保守: システム保守・運用業務
  • 研修: 研修・学習業務
  • 会議: 会議・打ち合わせ業務

トラブルシューティング

Gmail送信制限

// 送信制限を確認
checkEmailQuota();
  • 無料アカウント: 1日100通まで
  • Google Workspace: 1日1500通まで

メール送信テスト

// テスト送信
testEmailSending(); // メールアドレスを実際のものに変更

デバッグ方法

  1. GASエディタで「実行」→「実行ログ」を確認
  2. エラーの詳細情報をログで確認

カスタマイズポイント

メール本文のカスタマイズ

sendReportEmail関数のbody部分を編集

PDF レイアウトの変更

generatePDF関数でテーブル構造や装飾を変更

追加フィールドの作成

  1. initializeSheetのヘッダー配列に追加
  2. HTMLフォームに入力欄を追加
  3. submitReportrowData配列に追加

まとめ

このシステムにより、日報業務の自動化が実現できます:

  • 時間短縮: 手動作業を大幅削減
  • 📊 データ管理: スプレッドシートで一元管理
  • 🔍 追跡可能: メール送信状況を記録
  • 💰 コスト削減: 無料で運用可能

業務効率化の第一歩として、ぜひお試しください!

参考リンク


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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?