Google Apps Scriptで作る本格的な日報管理システム【メール送信・PDF生成・スプレッドシート連携】

## はじめに

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

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

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

## 完成イメージ

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

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

## システム構築手順

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

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

### 2. メインコードの実装

以下のコードを`Code.gs`に貼り付けます：

```javascript
// ==================================================
// 日報管理システム - メイン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`）を作成します：

```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. 初回セットアップ
```javascript
// GASエディタで実行
initializeSheet();
```

### 2. 権限の許可
初回実行時にGoogleから権限要求があります：
- Gmail送信権限
- Drive書き込み権限
- Sheets編集権限

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

## 主要機能の詳細

### メール送信状況の追跡

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

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

### PDF自動生成

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

### テンプレート機能

4種類の業務テンプレートを用意：
- **開発**: システム開発業務
- **保守**: システム保守・運用業務
- **研修**: 研修・学習業務
- **会議**: 会議・打ち合わせ業務

## トラブルシューティング

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

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

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

## カスタマイズポイント

### メール本文のカスタマイズ
`sendReportEmail`関数の`body`部分を編集

### PDF レイアウトの変更
`generatePDF`関数でテーブル構造や装飾を変更

### 追加フィールドの作成
1. `initializeSheet`のヘッダー配列に追加
2. HTMLフォームに入力欄を追加
3. `submitReport`の`rowData`配列に追加

## まとめ

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

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

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

## 参考リンク

- [Google Apps Script公式ドキュメント](https://developers.google.com/apps-script)
- [HtmlService Guide](https://developers.google.com/apps-script/guides/html)
- [MailApp Reference](https://developers.google.com/apps-script/reference/mail/mail-app)

---