「単純なメールのやり取りから解放されたい…」
「お客様との日程調整に毎日何時間も費やしている…」
「もっと本業に集中したいのに、事務作業に追われている…」
そんな悩みを抱えるあなたに、今すぐ実践できる革命的な解決策があります。
もし、お問い合わせへの返信から、カレンダーの空き時間を確認し、最適な日程を自動で確定、さらにはお客様への確認メールやリマインダーまで、すべてが”全自動”で完結するとしたらどうでしょうか?
この記事では、今話題のAI「Claude」とGoogleの無料ツール「Google Apps Script (GAS)」を組み合わせることで、面倒な営業の初期対応を完全に自動化する画期的な方法を、AI初心者の方でも絶対に理解できるよう、スクリーンショット付きでどこよりも詳しく解説します。
プログラミング知識ゼロでも大丈夫です。 この記事を読み終える頃には、あなたもAIを自在に操り、ビジネスを加速させる”未来の働き方”を手にしているはずです。
目次
- 全体像:AIが実現する未来の営業フロー
- 構築の核心:Claudeとの対話でコードを生成
- セットアップ手順:3つのステップで自動化を実現
- 自動生成されるもの一覧
- カスタマイズ方法
- まとめ:AIと共に創る、新しい働き方へ
1. 全体像:AIが実現する未来の営業フロー
今回構築するシステムは、お客様がチラシのQRコードを読み取ってフォームに情報を入力するだけで、以下のプロセスがすべて自動で実行されます。
自動化の流れ
| ステップ | 処理内容 | 担当 |
|---|---|---|
| 1 | QRコード入りのチラシを配布 | あなた(初回のみ) |
| 2 | お客様がQRコードを読み取り、Googleフォームに情報を入力 | お客様 |
| 3 | 入力された情報がスプレッドシートに自動集計 | |
| 4 | フォーム内の希望日程とGoogleカレンダーの空き状況を照合 | GAS(自動) |
| 5 | 最適なアポ日程を自動確定 | GAS(自動) |
| 6 | 確定した日程をGoogleカレンダーに自動登録 | GAS(自動) |
| 7 | お客様に確定日程を記載した確認メールを自動送信 | GAS(自動) |
| 8 | アポ前日の12時にリマインダーメールを自動送信 | GAS(自動) |
これにより、あなたは日程調整のプロセスに一切関わることなく、確定したアポイントだけがカレンダーに自動で入ってくる状態を実現できます。
システム全体像
以下の図は、システム全体の流れを視覚的に表したものです。

ポイント: チラシのQRコードからGoogleフォームへ、そしてスプレッドシートへデータが流れ、GASが自動でカレンダー登録とメール送信を行います。
2. 構築の核心:Claudeとの対話でコードを生成
この複雑に見えるシステムの構築は、驚くほど簡単です。なぜなら、システムの根幹となるプログラムコードは、AIであるClaudeがすべて生成してくれるからです。あなたがやるべきことは、やりたいことを日本語で具体的に伝えるだけです。
Claudeへの指示例
今回のシステムは、以下のような指示(プロンプト)をClaudeに与えることで作成されました。

実際に使用したプロンプトの内容は以下の通りです:
光回線の営業フローを自動化したいです。
①QRコード入りのチラシを巻く
②お客様がQRコードを読んで、Googleフォームに情報を入力する
③自動でスプシに集計される
④フォーム内に記載の日程候補日と、自分のGoogleカレンダーの空いている日程を照らし合わせて、アポ日程を確定する
⑤アポ日程を自分のGoogleカレンダーに登録する。
⑥お客様に、確定したアポ日程をメールでお客様に連絡する。
⑦アポ日程の前日12時にお客様にメールでリマインドする。上記のステップを、GASを使って完全に自動化したいので、ステップを詳しく解説しながら、GASを構築してください。
Claudeが生成するファイル
この指示に基づき、Claudeはシステムの設計図を描き、必要なファイル一式を自動で生成します。
実際に生成されたスクリプトファイル
/**
* =====================================================
* 光回線営業フロー 完全自動化システム
* =====================================================
*
* 【機能】
* 1. Googleフォームを自動生成
* 2. スプレッドシートを自動フォーマット
* 3. フォーム送信時に自動でカレンダー空き確認
* 4. 最適な日程を自動確定
* 5. Googleカレンダーに予定登録
* 6. お客様に確認メール送信
* 7. 前日12時にリマインドメール送信
*/
// =====================================================
// プロパティキー
// =====================================================
const PROPERTY_KEYS = {
CONFIG: 'SALES_CONFIG',
SPREADSHEET_ID: 'SPREADSHEET_ID',
FORM_ID: 'FORM_ID',
FORM_URL: 'FORM_URL',
SETUP_COMPLETE: 'SETUP_COMPLETE'
};
// ステータス定義
const STATUS = {
PENDING: '未処理',
CONFIRMED: 'アポ確定',
REMINDER_SENT: 'リマインド送信済',
COMPLETED: '完了',
NO_AVAILABILITY: '日程調整中',
ERROR: 'エラー'
};
// デフォルト設定
const DEFAULT_CONFIG = {
calendarId: '',
salesPersonName: '営業担当',
companyName: 'NEXT INNOVATION株式会社',
companyPhone: '03-XXXX-XXXX',
replyEmail: '',
appointmentDuration: 60,
businessHoursStart: 10,
businessHoursEnd: 19,
sheetName: 'フォームの回答 1'
};
// 列番号の設定(固定)
const COLUMNS = {
TIMESTAMP: 1,
NAME: 2,
EMAIL: 3,
PHONE: 4,
ADDRESS: 5,
PREFERRED_1: 6,
PREFERRED_2: 7,
PREFERRED_3: 8,
NOTES: 9,
CONFIRMED_DATE: 10,
STATUS: 11,
EVENT_ID: 12,
PROCESSED_AT: 13
};
// =====================================================
// Webアプリ関連
// =====================================================
/**
* Webアプリのエントリーポイント
*/
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('光回線営業フロー自動化システム')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
/**
* 現在の設定を取得
*/
function getConfig() {
const props = PropertiesService.getScriptProperties();
const configStr = props.getProperty(PROPERTY_KEYS.CONFIG);
if (configStr) {
return JSON.parse(configStr);
}
// デフォルト設定にユーザーのメールアドレスを設定
const userEmail = Session.getActiveUser().getEmail();
const config = { ...DEFAULT_CONFIG };
config.calendarId = userEmail;
config.replyEmail = userEmail;
return config;
}
/**
* 設定を保存
*/
function saveConfig(config) {
const props = PropertiesService.getScriptProperties();
props.setProperty(PROPERTY_KEYS.CONFIG, JSON.stringify(config));
return { success: true, message: '設定を保存しました' };
}
/**
* セットアップ状態を取得
*/
function getSetupStatus() {
const props = PropertiesService.getScriptProperties();
return {
isSetupComplete: props.getProperty(PROPERTY_KEYS.SETUP_COMPLETE) === 'true',
spreadsheetId: props.getProperty(PROPERTY_KEYS.SPREADSHEET_ID) || '',
formId: props.getProperty(PROPERTY_KEYS.FORM_ID) || '',
formUrl: props.getProperty(PROPERTY_KEYS.FORM_URL) || ''
};
}
// =====================================================
// フォーム・スプレッドシート自動生成
// =====================================================
/**
* フォームとスプレッドシートを自動生成
*/
function createFormAndSpreadsheet(config) {
try {
// 設定を保存
saveConfig(config);
// 1. スプレッドシートを作成
const ss = SpreadsheetApp.create('光回線営業管理_' + Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyyMMdd_HHmmss'));
const spreadsheetId = ss.getId();
const spreadsheetUrl = ss.getUrl();
Logger.log('📊 スプレッドシート作成: ' + spreadsheetId);
// 2. Googleフォームを作成
const form = FormApp.create('光回線サービス お問い合わせフォーム');
const formId = form.getId();
// フォームの設定
form.setDescription('光回線サービスの訪問説明をご希望の方は、以下のフォームにご記入ください。\nご希望の日時を確認し、担当者よりご連絡いたします。');
form.setConfirmationMessage('お問い合わせありがとうございます。\nご希望の日時を確認し、確定次第メールにてご連絡いたします。');
form.setCollectEmail(false);
// 質問を追加
form.addTextItem()
.setTitle('お名前')
.setRequired(true)
.setHelpText('例: 山田 太郎');
form.addTextItem()
.setTitle('メールアドレス')
.setRequired(true)
.setHelpText('確認メールをお送りします');
form.addTextItem()
.setTitle('電話番号')
.setRequired(true)
.setHelpText('例: 090-1234-5678');
form.addParagraphTextItem()
.setTitle('ご住所')
.setRequired(true)
.setHelpText('訪問先のご住所をご記入ください');
form.addDateTimeItem()
.setTitle('第1希望日時')
.setRequired(true)
.setHelpText('ご希望の訪問日時をお選びください');
form.addDateTimeItem()
.setTitle('第2希望日時')
.setRequired(false)
.setHelpText('第1希望が難しい場合の候補');
form.addDateTimeItem()
.setTitle('第3希望日時')
.setRequired(false)
.setHelpText('第2希望が難しい場合の候補');
form.addParagraphTextItem()
.setTitle('ご要望・備考')
.setRequired(false)
.setHelpText('その他ご要望がありましたらご記入ください');
// フォームをスプレッドシートにリンク
form.setDestination(FormApp.DestinationType.SPREADSHEET, spreadsheetId);
const formUrl = form.getPublishedUrl();
const formEditUrl = form.getEditUrl();
Logger.log('📝 フォーム作成: ' + formId);
Logger.log('🔗 フォームURL: ' + formUrl);
// 少し待機(フォームとスプレッドシートの連携を待つ)
Utilities.sleep(3000);
// 3. スプレッドシートのフォーマット設定
setupSpreadsheetFormat(ss, config);
// 4. プロパティに保存
const props = PropertiesService.getScriptProperties();
props.setProperty(PROPERTY_KEYS.SPREADSHEET_ID, spreadsheetId);
props.setProperty(PROPERTY_KEYS.FORM_ID, formId);
props.setProperty(PROPERTY_KEYS.FORM_URL, formUrl);
// 5. トリガーを設定
setupTriggersForSpreadsheet(spreadsheetId);
// 完了フラグを設定
props.setProperty(PROPERTY_KEYS.SETUP_COMPLETE, 'true');
return {
success: true,
message: 'セットアップが完了しました!',
data: {
spreadsheetId: spreadsheetId,
spreadsheetUrl: spreadsheetUrl,
formId: formId,
formUrl: formUrl,
formEditUrl: formEditUrl
}
};
} catch (error) {
Logger.log('❌ セットアップエラー: ' + error.message);
console.error(error);
return {
success: false,
message: 'エラーが発生しました: ' + error.message
};
}
}
/**
* スプレッドシートのフォーマットを設定
*/
function setupSpreadsheetFormat(ss, config) {
// フォーム回答シートを取得(少し待機が必要な場合がある)
Utilities.sleep(2000);
let sheet = ss.getSheets()[0];
// シート名を設定
const sheetName = config.sheetName || 'フォームの回答 1';
// 追加列のヘッダーを設定
const headers = ['確定日時', 'ステータス', 'イベントID', '処理日時'];
const startCol = COLUMNS.CONFIRMED_DATE;
for (let i = 0; i < headers.length; i++) {
sheet.getRange(1, startCol + i).setValue(headers[i]);
}
// ヘッダー行の書式設定
const headerRange = sheet.getRange(1, 1, 1, COLUMNS.PROCESSED_AT);
headerRange.setBackground('#4285f4');
headerRange.setFontColor('#ffffff');
headerRange.setFontWeight('bold');
// 列幅の調整
sheet.setColumnWidth(COLUMNS.NAME, 120);
sheet.setColumnWidth(COLUMNS.EMAIL, 200);
sheet.setColumnWidth(COLUMNS.PHONE, 130);
sheet.setColumnWidth(COLUMNS.ADDRESS, 250);
sheet.setColumnWidth(COLUMNS.PREFERRED_1, 150);
sheet.setColumnWidth(COLUMNS.PREFERRED_2, 150);
sheet.setColumnWidth(COLUMNS.PREFERRED_3, 150);
sheet.setColumnWidth(COLUMNS.CONFIRMED_DATE, 150);
sheet.setColumnWidth(COLUMNS.STATUS, 120);
// ステータス列の条件付き書式
const statusRange = sheet.getRange('K2:K1000');
// 条件付き書式ルールを作成
const rules = sheet.getConditionalFormatRules();
// アポ確定 → 緑
rules.push(SpreadsheetApp.newConditionalFormatRule()
.whenTextEqualTo(STATUS.CONFIRMED)
.setBackground('#c6efce')
.setRanges([statusRange])
.build());
// リマインド送信済 → 青
rules.push(SpreadsheetApp.newConditionalFormatRule()
.whenTextEqualTo(STATUS.REMINDER_SENT)
.setBackground('#bdd7ee')
.setRanges([statusRange])
.build());
// 日程調整中 → 黄
rules.push(SpreadsheetApp.newConditionalFormatRule()
.whenTextEqualTo(STATUS.NO_AVAILABILITY)
.setBackground('#ffeb9c')
.setRanges([statusRange])
.build());
// エラー → 赤
rules.push(SpreadsheetApp.newConditionalFormatRule()
.whenTextEqualTo(STATUS.ERROR)
.setBackground('#ffc7ce')
.setRanges([statusRange])
.build());
sheet.setConditionalFormatRules(rules);
// フィルターを設定
sheet.getRange(1, 1, 1, COLUMNS.PROCESSED_AT).createFilter();
// 1行目を固定
sheet.setFrozenRows(1);
Logger.log('✅ スプレッドシートのフォーマット設定完了');
}
/**
* トリガーを設定
*/
function setupTriggersForSpreadsheet(spreadsheetId) {
// 既存のトリガーを削除
const triggers = ScriptApp.getProjectTriggers();
triggers.forEach(trigger => {
const handlerFunction = trigger.getHandlerFunction();
if (['onFormSubmit', 'sendReminderEmails', 'processUnprocessedEntries'].includes(handlerFunction)) {
ScriptApp.deleteTrigger(trigger);
}
});
// フォーム送信時トリガー
const ss = SpreadsheetApp.openById(spreadsheetId);
ScriptApp.newTrigger('onFormSubmit')
.forSpreadsheet(ss)
.onFormSubmit()
.create();
// 毎日11:50に実行(12時のリマインド用)
ScriptApp.newTrigger('sendReminderEmails')
.timeBased()
.everyDays(1)
.atHour(11)
.nearMinute(50)
.create();
// 毎時実行(未処理データの再処理用)
ScriptApp.newTrigger('processUnprocessedEntries')
.timeBased()
.everyHours(1)
.create();
Logger.log('✅ トリガー設定完了');
}
// =====================================================
// QRコード生成
// =====================================================
/**
* QRコードのURLを生成
*/
function generateQRCodeUrl(formUrl, size) {
const qrSize = size || 300;
// Google Charts APIを使用
return `https://chart.googleapis.com/chart?cht=qr&chs=${qrSize}x${qrSize}&chl=${encodeURIComponent(formUrl)}`;
}
// =====================================================
// メイン処理関数
// =====================================================
/**
* フォーム送信時に実行される関数
*/
function onFormSubmit(e) {
try {
const props = PropertiesService.getScriptProperties();
const spreadsheetId = props.getProperty(PROPERTY_KEYS.SPREADSHEET_ID);
if (!spreadsheetId) {
Logger.log('❌ スプレッドシートIDが設定されていません');
return;
}
const ss = SpreadsheetApp.openById(spreadsheetId);
const config = getConfig();
const sheet = ss.getSheetByName(config.sheetName) || ss.getSheets()[0];
const row = e.range.getRow();
Logger.log(`📝 新規フォーム送信を検知: 行 ${row}`);
// 処理実行
processAppointment(sheet, row, config);
} catch (error) {
Logger.log('❌ フォーム送信処理エラー: ' + error.message);
console.error(error);
}
}
/**
* アポイント処理のメイン関数
*/
function processAppointment(sheet, row, config) {
// すでに処理済みかチェック
const currentStatus = sheet.getRange(row, COLUMNS.STATUS).getValue();
if (currentStatus && currentStatus !== STATUS.PENDING && currentStatus !== STATUS.NO_AVAILABILITY) {
Logger.log(`⏭️ 行 ${row} は処理済みです: ${currentStatus}`);
return;
}
// 顧客データを取得
const customerData = {
name: sheet.getRange(row, COLUMNS.NAME).getValue(),
email: sheet.getRange(row, COLUMNS.EMAIL).getValue(),
phone: sheet.getRange(row, COLUMNS.PHONE).getValue(),
address: sheet.getRange(row, COLUMNS.ADDRESS).getValue(),
preferred1: sheet.getRange(row, COLUMNS.PREFERRED_1).getValue(),
preferred2: sheet.getRange(row, COLUMNS.PREFERRED_2).getValue(),
preferred3: sheet.getRange(row, COLUMNS.PREFERRED_3).getValue(),
notes: sheet.getRange(row, COLUMNS.NOTES).getValue()
};
Logger.log(`👤 顧客: ${customerData.name} (${customerData.email})`);
// 希望日時のリストを作成
const preferredDates = [
customerData.preferred1,
customerData.preferred2,
customerData.preferred3
].filter(date => date && date !== '');
if (preferredDates.length === 0) {
Logger.log('❌ 希望日時が入力されていません');
sheet.getRange(row, COLUMNS.STATUS).setValue(STATUS.ERROR);
return;
}
// カレンダーの空き状況を確認して最適な日程を取得
const confirmedDate = findAvailableSlot(preferredDates, config);
if (!confirmedDate) {
Logger.log('⚠️ 希望日時に空きがありません');
sheet.getRange(row, COLUMNS.STATUS).setValue(STATUS.NO_AVAILABILITY);
sendNoAvailabilityEmail(customerData, config);
return;
}
Logger.log(`✅ 確定日時: ${confirmedDate}`);
// Googleカレンダーに予定を登録
const eventId = createCalendarEvent(customerData, confirmedDate, config);
if (!eventId) {
Logger.log('❌ カレンダー登録に失敗しました');
sheet.getRange(row, COLUMNS.STATUS).setValue(STATUS.ERROR);
return;
}
// 確認メールを送信
sendConfirmationEmail(customerData, confirmedDate, config);
// スプレッドシートを更新
sheet.getRange(row, COLUMNS.CONFIRMED_DATE).setValue(confirmedDate);
sheet.getRange(row, COLUMNS.STATUS).setValue(STATUS.CONFIRMED);
sheet.getRange(row, COLUMNS.EVENT_ID).setValue(eventId);
sheet.getRange(row, COLUMNS.PROCESSED_AT).setValue(new Date());
Logger.log(`🎉 処理完了: ${customerData.name}様のアポイントを ${formatDateTime(confirmedDate)} に確定`);
}
// =====================================================
// カレンダー関連関数
// =====================================================
/**
* 希望日時リストから空いている日時を探す
*/
function findAvailableSlot(preferredDates, config) {
const calendar = CalendarApp.getCalendarById(config.calendarId);
if (!calendar) {
Logger.log('❌ カレンダーが見つかりません: ' + config.calendarId);
return null;
}
for (const preferredDate of preferredDates) {
if (!preferredDate) continue;
const startTime = new Date(preferredDate);
const endTime = new Date(startTime.getTime() + config.appointmentDuration * 60 * 1000);
// 過去の日付はスキップ
if (startTime < new Date()) {
Logger.log(`⏭️ 過去の日付をスキップ: ${formatDateTime(startTime)}`);
continue;
}
// 営業時間内かチェック
const hour = startTime.getHours();
if (hour < config.businessHoursStart || hour >= config.businessHoursEnd) {
Logger.log(`⏭️ 営業時間外をスキップ: ${formatDateTime(startTime)}`);
continue;
}
// その時間帯に予定があるかチェック
const events = calendar.getEvents(startTime, endTime);
if (events.length === 0) {
Logger.log(`✅ 空き確認OK: ${formatDateTime(startTime)}`);
return startTime;
} else {
Logger.log(`❌ 予定あり: ${formatDateTime(startTime)} (${events.length}件)`);
}
}
return null;
}
/**
* Googleカレンダーに予定を登録
*/
function createCalendarEvent(customerData, appointmentDate, config) {
try {
const calendar = CalendarApp.getCalendarById(config.calendarId);
if (!calendar) {
Logger.log('❌ カレンダーが見つかりません');
return null;
}
const startTime = new Date(appointmentDate);
const endTime = new Date(startTime.getTime() + config.appointmentDuration * 60 * 1000);
const title = `【光回線】${customerData.name}様 訪問アポ`;
const description = `
■ お客様情報
━━━━━━━━━━━━━━━━━━
お名前: ${customerData.name}
メール: ${customerData.email}
電話番号: ${customerData.phone}
ご住所: ${customerData.address}
■ ご要望・備考
━━━━━━━━━━━━━━━━━━
${customerData.notes || 'なし'}
■ 対応メモ
━━━━━━━━━━━━━━━━━━
(ここに対応内容を記録)
`.trim();
const event = calendar.createEvent(title, startTime, endTime, {
description: description,
location: customerData.address
});
// リマインダーを設定
event.removeAllReminders();
event.addPopupReminder(60 * 24); // 24時間前
event.addPopupReminder(60); // 1時間前
Logger.log(`📅 カレンダー登録完了: ${event.getId()}`);
return event.getId();
} catch (error) {
Logger.log('❌ カレンダー登録エラー: ' + error.message);
return null;
}
}
// =====================================================
// メール送信関数
// =====================================================
/**
* アポイント確認メールを送信
*/
function sendConfirmationEmail(customerData, appointmentDate, config) {
const formattedDate = formatDateTime(appointmentDate);
const subject = `【${config.companyName}】訪問日時確定のご連絡`;
const body = `
${customerData.name} 様
この度は、光回線サービスにご興味をお持ちいただき、
誠にありがとうございます。
下記の日程にて、ご訪問させていただきます。
━━━━━━━━━━━━━━━━━━━━━━━━━━━
■ 訪問日時
${formattedDate}
(所要時間:約${config.appointmentDuration}分)
■ 訪問先
${customerData.address}
━━━━━━━━━━━━━━━━━━━━━━━━━━━
当日は、現在のインターネット環境についてお伺いし、
最適なプランをご提案させていただきます。
ご不明な点やご変更がございましたら、
お気軽にご連絡ください。
━━━━━━━━━━━━━━━━━━━━━━━━━━━
${config.companyName}
担当: ${config.salesPersonName}
TEL: ${config.companyPhone}
Email: ${config.replyEmail}
━━━━━━━━━━━━━━━━━━━━━━━━━━━
※このメールは自動送信されています。
`.trim();
try {
GmailApp.sendEmail(customerData.email, subject, body, {
name: config.companyName,
replyTo: config.replyEmail
});
Logger.log(`📧 確認メール送信完了: ${customerData.email}`);
return true;
} catch (error) {
Logger.log('❌ メール送信エラー: ' + error.message);
return false;
}
}
/**
* 日程調整が必要な場合のメール送信
*/
function sendNoAvailabilityEmail(customerData, config) {
const subject = `【${config.companyName}】訪問日程のご相談`;
const body = `
${customerData.name} 様
この度は、光回線サービスにご興味をお持ちいただき、
誠にありがとうございます。
大変恐れ入りますが、ご希望いただいた日時に
先約がございました。
改めて日程を調整させていただきたく、
担当者より折り返しご連絡させていただきます。
ご不便をおかけいたしますが、
何卒よろしくお願い申し上げます。
━━━━━━━━━━━━━━━━━━━━━━━━━━━
${config.companyName}
担当: ${config.salesPersonName}
TEL: ${config.companyPhone}
Email: ${config.replyEmail}
━━━━━━━━━━━━━━━━━━━━━━━━━━━
※このメールは自動送信されています。
`.trim();
try {
GmailApp.sendEmail(customerData.email, subject, body, {
name: config.companyName,
replyTo: config.replyEmail
});
Logger.log(`📧 日程調整メール送信完了: ${customerData.email}`);
return true;
} catch (error) {
Logger.log('❌ メール送信エラー: ' + error.message);
return false;
}
}
/**
* リマインドメールを送信(毎日11:50に実行)
*/
function sendReminderEmails() {
Logger.log('🔔 リマインドメール送信処理を開始');
const props = PropertiesService.getScriptProperties();
const spreadsheetId = props.getProperty(PROPERTY_KEYS.SPREADSHEET_ID);
if (!spreadsheetId) {
Logger.log('スプレッドシートIDが設定されていません');
return;
}
const config = getConfig();
const ss = SpreadsheetApp.openById(spreadsheetId);
const sheet = ss.getSheetByName(config.sheetName) || ss.getSheets()[0];
const lastRow = sheet.getLastRow();
if (lastRow < 2) {
Logger.log('データがありません');
return;
}
// 明日の日付範囲を計算
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
const dayAfterTomorrow = new Date(tomorrow);
dayAfterTomorrow.setDate(dayAfterTomorrow.getDate() + 1);
let reminderCount = 0;
// 各行をチェック
for (let row = 2; row <= lastRow; row++) {
const status = sheet.getRange(row, COLUMNS.STATUS).getValue();
if (status !== STATUS.CONFIRMED) continue;
const confirmedDate = sheet.getRange(row, COLUMNS.CONFIRMED_DATE).getValue();
if (!confirmedDate) continue;
const appointmentDate = new Date(confirmedDate);
// 明日のアポかチェック
if (appointmentDate >= tomorrow && appointmentDate < dayAfterTomorrow) {
const customerData = {
name: sheet.getRange(row, COLUMNS.NAME).getValue(),
email: sheet.getRange(row, COLUMNS.EMAIL).getValue(),
address: sheet.getRange(row, COLUMNS.ADDRESS).getValue()
};
const sent = sendReminderEmail(customerData, appointmentDate, config);
if (sent) {
sheet.getRange(row, COLUMNS.STATUS).setValue(STATUS.REMINDER_SENT);
reminderCount++;
Logger.log(`✅ リマインド送信: ${customerData.name}様`);
}
}
}
Logger.log(`🔔 リマインドメール送信完了: ${reminderCount}件`);
}
/**
* リマインドメールの送信
*/
function sendReminderEmail(customerData, appointmentDate, config) {
const formattedDate = formatDateTime(appointmentDate);
const subject = `【明日のご予約】${config.companyName} 訪問のご確認`;
const body = `
${customerData.name} 様
いつもお世話になっております。
${config.companyName}でございます。
明日のご訪問について、リマインドのご連絡です。
━━━━━━━━━━━━━━━━━━━━━━━━━━━
■ 訪問日時
${formattedDate}
■ 訪問先
${customerData.address}
━━━━━━━━━━━━━━━━━━━━━━━━━━━
ご都合が悪くなった場合は、お早めにご連絡ください。
明日お会いできることを楽しみにしております。
━━━━━━━━━━━━━━━━━━━━━━━━━━━
${config.companyName}
担当: ${config.salesPersonName}
TEL: ${config.companyPhone}
Email: ${config.replyEmail}
━━━━━━━━━━━━━━━━━━━━━━━━━━━
※このメールは自動送信されています。
`.trim();
try {
GmailApp.sendEmail(customerData.email, subject, body, {
name: config.companyName,
replyTo: config.replyEmail
});
return true;
} catch (error) {
Logger.log('❌ リマインドメール送信エラー: ' + error.message);
return false;
}
}
/**
* 未処理のエントリを再処理(毎時実行)
*/
function processUnprocessedEntries() {
Logger.log('🔄 未処理データの再処理を開始');
const props = PropertiesService.getScriptProperties();
const spreadsheetId = props.getProperty(PROPERTY_KEYS.SPREADSHEET_ID);
if (!spreadsheetId) {
Logger.log('スプレッドシートIDが設定されていません');
return;
}
const config = getConfig();
const ss = SpreadsheetApp.openById(spreadsheetId);
const sheet = ss.getSheetByName(config.sheetName) || ss.getSheets()[0];
const lastRow = sheet.getLastRow();
if (lastRow < 2) {
Logger.log('データがありません');
return;
}
let processedCount = 0;
for (let row = 2; row <= lastRow; row++) {
const status = sheet.getRange(row, COLUMNS.STATUS).getValue();
if (!status || status === STATUS.PENDING || status === STATUS.NO_AVAILABILITY) {
processAppointment(sheet, row, config);
processedCount++;
Utilities.sleep(1000);
}
}
Logger.log(`🔄 再処理完了: ${processedCount}件`);
}
// =====================================================
// ユーティリティ関数
// =====================================================
/**
* 日時を読みやすい形式にフォーマット
*/
function formatDateTime(date) {
const d = new Date(date);
const weekdays = ['日', '月', '火', '水', '木', '金', '土'];
const year = d.getFullYear();
const month = d.getMonth() + 1;
const day = d.getDate();
const weekday = weekdays[d.getDay()];
const hour = String(d.getHours()).padStart(2, '0');
const minute = String(d.getMinutes()).padStart(2, '0');
return `${year}年${month}月${day}日(${weekday}) ${hour}:${minute}`;
}
// =====================================================
// テスト・デバッグ用関数
// =====================================================
/**
* カレンダー接続テスト
*/
function testCalendarConnection() {
const config = getConfig();
const calendar = CalendarApp.getCalendarById(config.calendarId);
if (calendar) {
return { success: true, message: 'カレンダー接続成功: ' + calendar.getName() };
} else {
return { success: false, message: 'カレンダーが見つかりません: ' + config.calendarId };
}
}
/**
* メール送信テスト
*/
function testEmailSend() {
const config = getConfig();
const testEmail = Session.getActiveUser().getEmail();
try {
GmailApp.sendEmail(testEmail, 'テストメール - 光回線営業システム', 'これはテストメールです。システムは正常に動作しています。', {
name: config.companyName
});
return { success: true, message: 'テストメール送信完了: ' + testEmail };
} catch (error) {
return { success: false, message: 'メール送信エラー: ' + error.message };
}
}
/**
* 設定をリセット
*/
function resetAllSettings() {
const props = PropertiesService.getScriptProperties();
props.deleteAllProperties();
// トリガーも削除
const triggers = ScriptApp.getProjectTriggers();
triggers.forEach(trigger => ScriptApp.deleteTrigger(trigger));
return { success: true, message: '全ての設定をリセットしました' };
}
実際に生成されたhtmlファイル
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>光回線営業フロー自動化システム</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans JP', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
}
.header {
text-align: center;
color: white;
margin-bottom: 30px;
}
.header h1 {
font-size: 28px;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.header p {
font-size: 14px;
opacity: 0.9;
}
.card {
background: white;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 30px;
margin-bottom: 20px;
}
.card-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 700;
color: #333;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #f0f0f0;
}
.card-title .material-icons {
color: #667eea;
font-size: 24px;
}
.form-group {
margin-bottom: 20px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 600px) {
.form-row {
grid-template-columns: 1fr;
}
}
label {
display: block;
font-size: 13px;
font-weight: 500;
color: #555;
margin-bottom: 6px;
}
label .required {
color: #e74c3c;
margin-left: 4px;
}
input, select {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
font-family: inherit;
transition: all 0.3s ease;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 14px 28px;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
}
.btn-primary:disabled {
background: #ccc;
box-shadow: none;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: #f5f5f5;
color: #333;
}
.btn-secondary:hover {
background: #e8e8e8;
}
.btn-success {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
color: white;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 20px;
}
.status-box {
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
}
.status-box.success {
background: linear-gradient(135deg, rgba(17, 153, 142, 0.1) 0%, rgba(56, 239, 125, 0.1) 100%);
border: 2px solid #11998e;
}
.status-box.pending {
background: #fff9e6;
border: 2px solid #f39c12;
}
.status-box h3 {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
margin-bottom: 15px;
}
.status-box.success h3 {
color: #11998e;
}
.status-box.pending h3 {
color: #f39c12;
}
.link-box {
background: #f8f9fa;
border-radius: 8px;
padding: 12px 15px;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.link-box label {
margin-bottom: 0;
white-space: nowrap;
}
.link-box a {
color: #667eea;
text-decoration: none;
word-break: break-all;
font-size: 13px;
}
.link-box a:hover {
text-decoration: underline;
}
.qr-section {
text-align: center;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
margin-top: 20px;
}
.qr-section img {
max-width: 200px;
border: 4px solid white;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.qr-section p {
margin-top: 15px;
font-size: 13px;
color: #666;
}
.loading {
display: none;
text-align: center;
padding: 40px;
}
.loading.show {
display: block;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.alert {
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
}
.alert.show {
display: block;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.steps {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
position: relative;
}
.steps::before {
content: '';
position: absolute;
top: 20px;
left: 50px;
right: 50px;
height: 3px;
background: #e0e0e0;
z-index: 1;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 2;
}
.step-number {
width: 40px;
height: 40px;
border-radius: 50%;
background: #e0e0e0;
color: #999;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
margin-bottom: 8px;
}
.step.active .step-number {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.step.completed .step-number {
background: #11998e;
color: white;
}
.step-label {
font-size: 12px;
color: #999;
}
.step.active .step-label,
.step.completed .step-label {
color: #333;
font-weight: 500;
}
.help-text {
font-size: 12px;
color: #888;
margin-top: 5px;
}
.section-divider {
height: 1px;
background: #e0e0e0;
margin: 25px 0;
}
.test-results {
background: #f8f9fa;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
font-family: monospace;
font-size: 13px;
white-space: pre-wrap;
display: none;
}
.test-results.show {
display: block;
}
.hidden {
display: none !important;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 光回線営業フロー自動化システム</h1>
<p>Googleフォーム・スプレッドシート・カレンダー連携を一括セットアップ</p>
</div>
<!-- アラート -->
<div id="alert" class="alert"></div>
<!-- ステップインジケーター -->
<div class="card">
<div class="steps">
<div class="step" id="step1">
<div class="step-number">1</div>
<div class="step-label">設定入力</div>
</div>
<div class="step" id="step2">
<div class="step-number">2</div>
<div class="step-label">自動生成</div>
</div>
<div class="step" id="step3">
<div class="step-number">3</div>
<div class="step-label">完了</div>
</div>
</div>
</div>
<!-- セットアップ完了時の表示 -->
<div id="completedSection" class="hidden">
<div class="card">
<div class="status-box success">
<h3><span class="material-icons">check_circle</span> セットアップ完了</h3>
<p>システムは正常に稼働しています。以下のリンクからアクセスできます。</p>
</div>
<div class="link-box">
<label>📝 お問い合わせフォーム:</label>
<a href="#" id="formUrlLink" target="_blank">-</a>
</div>
<div class="link-box">
<label>📊 管理スプレッドシート:</label>
<a href="#" id="spreadsheetUrlLink" target="_blank">-</a>
</div>
<div class="link-box">
<label>⚙️ フォーム編集:</label>
<a href="#" id="formEditUrlLink" target="_blank">-</a>
</div>
<div class="qr-section">
<p style="margin-bottom: 15px; font-weight: 500;">📱 チラシ用QRコード</p>
<img id="qrCodeImage" src="" alt="QRコード">
<p>このQRコードをチラシに印刷してください</p>
</div>
<div class="section-divider"></div>
<div class="card-title">
<span class="material-icons">build</span>
テスト・メンテナンス
</div>
<div class="btn-group">
<button class="btn btn-secondary" onclick="testCalendar()">
<span class="material-icons">event</span>
カレンダー接続テスト
</button>
<button class="btn btn-secondary" onclick="testEmail()">
<span class="material-icons">email</span>
メール送信テスト
</button>
<button class="btn btn-danger" onclick="confirmReset()">
<span class="material-icons">refresh</span>
設定リセット
</button>
</div>
<div id="testResults" class="test-results"></div>
</div>
</div>
<!-- 設定フォーム -->
<div id="setupSection">
<div class="card">
<div class="card-title">
<span class="material-icons">business</span>
会社情報
</div>
<div class="form-row">
<div class="form-group">
<label>会社名<span class="required">*</span></label>
<input type="text" id="companyName" placeholder="例: NEXT INNOVATION株式会社">
</div>
<div class="form-group">
<label>担当者名<span class="required">*</span></label>
<input type="text" id="salesPersonName" placeholder="例: 山田 太郎">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>会社電話番号<span class="required">*</span></label>
<input type="text" id="companyPhone" placeholder="例: 03-1234-5678">
</div>
<div class="form-group">
<label>返信用メールアドレス<span class="required">*</span></label>
<input type="email" id="replyEmail" placeholder="例: info@company.com">
</div>
</div>
</div>
<div class="card">
<div class="card-title">
<span class="material-icons">event</span>
カレンダー・営業時間設定
</div>
<div class="form-group">
<label>GoogleカレンダーID<span class="required">*</span></label>
<input type="text" id="calendarId" placeholder="例: your-email@gmail.com">
<p class="help-text">通常はGmailアドレスと同じです。共有カレンダーを使う場合はカレンダー設定画面でIDを確認してください。</p>
</div>
<div class="form-row">
<div class="form-group">
<label>アポイント所要時間(分)</label>
<select id="appointmentDuration">
<option value="30">30分</option>
<option value="45">45分</option>
<option value="60" selected>60分(1時間)</option>
<option value="90">90分</option>
<option value="120">120分(2時間)</option>
</select>
</div>
<div class="form-group">
<label>シート名</label>
<input type="text" id="sheetName" value="フォームの回答 1">
<p class="help-text">通常は変更不要です</p>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>営業開始時間</label>
<select id="businessHoursStart">
<option value="8">8:00</option>
<option value="9">9:00</option>
<option value="10" selected>10:00</option>
<option value="11">11:00</option>
</select>
</div>
<div class="form-group">
<label>営業終了時間</label>
<select id="businessHoursEnd">
<option value="17">17:00</option>
<option value="18">18:00</option>
<option value="19" selected>19:00</option>
<option value="20">20:00</option>
<option value="21">21:00</option>
</select>
</div>
</div>
</div>
<div class="card">
<div class="card-title">
<span class="material-icons">rocket_launch</span>
セットアップ実行
</div>
<p style="color: #666; margin-bottom: 20px; font-size: 14px;">
以下のボタンをクリックすると、Googleフォーム、スプレッドシート、トリガーが自動で作成されます。
初回実行時は権限の承認が必要です。
</p>
<div class="btn-group">
<button class="btn btn-primary" id="setupBtn" onclick="runSetup()">
<span class="material-icons">auto_fix_high</span>
自動セットアップを実行
</button>
<button class="btn btn-secondary" onclick="saveConfigOnly()">
<span class="material-icons">save</span>
設定のみ保存
</button>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>セットアップ中です...<br>フォームとスプレッドシートを作成しています</p>
</div>
</div>
</div>
</div>
<script>
// ページ読み込み時
document.addEventListener('DOMContentLoaded', function() {
loadConfig();
checkSetupStatus();
});
// 設定を読み込み
function loadConfig() {
google.script.run
.withSuccessHandler(function(config) {
document.getElementById('companyName').value = config.companyName || '';
document.getElementById('salesPersonName').value = config.salesPersonName || '';
document.getElementById('companyPhone').value = config.companyPhone || '';
document.getElementById('replyEmail').value = config.replyEmail || '';
document.getElementById('calendarId').value = config.calendarId || '';
document.getElementById('appointmentDuration').value = config.appointmentDuration || 60;
document.getElementById('businessHoursStart').value = config.businessHoursStart || 10;
document.getElementById('businessHoursEnd').value = config.businessHoursEnd || 19;
document.getElementById('sheetName').value = config.sheetName || 'フォームの回答 1';
})
.getConfig();
}
// セットアップ状態をチェック
function checkSetupStatus() {
google.script.run
.withSuccessHandler(function(status) {
if (status.isSetupComplete && status.formUrl) {
showCompletedSection(status);
} else {
showSetupSection();
}
})
.getSetupStatus();
}
// セットアップ画面を表示
function showSetupSection() {
document.getElementById('setupSection').classList.remove('hidden');
document.getElementById('completedSection').classList.add('hidden');
updateSteps(1);
}
// 完了画面を表示
function showCompletedSection(data) {
document.getElementById('setupSection').classList.add('hidden');
document.getElementById('completedSection').classList.remove('hidden');
if (data.formUrl) {
document.getElementById('formUrlLink').href = data.formUrl;
document.getElementById('formUrlLink').textContent = data.formUrl;
// QRコード生成
const qrUrl = 'https://chart.googleapis.com/chart?cht=qr&chs=300x300&chl=' + encodeURIComponent(data.formUrl);
document.getElementById('qrCodeImage').src = qrUrl;
}
if (data.spreadsheetId) {
const ssUrl = 'https://docs.google.com/spreadsheets/d/' + data.spreadsheetId;
document.getElementById('spreadsheetUrlLink').href = ssUrl;
document.getElementById('spreadsheetUrlLink').textContent = ssUrl;
}
if (data.formEditUrl) {
document.getElementById('formEditUrlLink').href = data.formEditUrl;
document.getElementById('formEditUrlLink').textContent = '編集画面を開く';
}
updateSteps(3);
}
// ステップ更新
function updateSteps(currentStep) {
for (let i = 1; i <= 3; i++) {
const step = document.getElementById('step' + i);
step.classList.remove('active', 'completed');
if (i < currentStep) {
step.classList.add('completed');
} else if (i === currentStep) {
step.classList.add('active');
}
}
}
// フォームから設定を取得
function getConfigFromForm() {
return {
companyName: document.getElementById('companyName').value,
salesPersonName: document.getElementById('salesPersonName').value,
companyPhone: document.getElementById('companyPhone').value,
replyEmail: document.getElementById('replyEmail').value,
calendarId: document.getElementById('calendarId').value,
appointmentDuration: parseInt(document.getElementById('appointmentDuration').value),
businessHoursStart: parseInt(document.getElementById('businessHoursStart').value),
businessHoursEnd: parseInt(document.getElementById('businessHoursEnd').value),
sheetName: document.getElementById('sheetName').value
};
}
// バリデーション
function validateConfig(config) {
if (!config.companyName) return '会社名を入力してください';
if (!config.salesPersonName) return '担当者名を入力してください';
if (!config.companyPhone) return '会社電話番号を入力してください';
if (!config.replyEmail) return '返信用メールアドレスを入力してください';
if (!config.calendarId) return 'カレンダーIDを入力してください';
return null;
}
// セットアップ実行
function runSetup() {
const config = getConfigFromForm();
const error = validateConfig(config);
if (error) {
showAlert(error, 'error');
return;
}
document.getElementById('setupBtn').disabled = true;
document.getElementById('loading').classList.add('show');
updateSteps(2);
google.script.run
.withSuccessHandler(function(result) {
document.getElementById('loading').classList.remove('show');
document.getElementById('setupBtn').disabled = false;
if (result.success) {
showAlert(result.message, 'success');
showCompletedSection({
formUrl: result.data.formUrl,
formEditUrl: result.data.formEditUrl,
spreadsheetId: result.data.spreadsheetId
});
} else {
showAlert(result.message, 'error');
updateSteps(1);
}
})
.withFailureHandler(function(error) {
document.getElementById('loading').classList.remove('show');
document.getElementById('setupBtn').disabled = false;
showAlert('エラーが発生しました: ' + error.message, 'error');
updateSteps(1);
})
.createFormAndSpreadsheet(config);
}
// 設定のみ保存
function saveConfigOnly() {
const config = getConfigFromForm();
google.script.run
.withSuccessHandler(function(result) {
showAlert(result.message, 'success');
})
.withFailureHandler(function(error) {
showAlert('エラー: ' + error.message, 'error');
})
.saveConfig(config);
}
// カレンダーテスト
function testCalendar() {
const resultsDiv = document.getElementById('testResults');
resultsDiv.textContent = 'カレンダー接続をテスト中...';
resultsDiv.classList.add('show');
google.script.run
.withSuccessHandler(function(result) {
resultsDiv.textContent = result.success ? '✅ ' + result.message : '❌ ' + result.message;
})
.withFailureHandler(function(error) {
resultsDiv.textContent = '❌ エラー: ' + error.message;
})
.testCalendarConnection();
}
// メールテスト
function testEmail() {
const resultsDiv = document.getElementById('testResults');
resultsDiv.textContent = 'メール送信をテスト中...';
resultsDiv.classList.add('show');
google.script.run
.withSuccessHandler(function(result) {
resultsDiv.textContent = result.success ? '✅ ' + result.message : '❌ ' + result.message;
})
.withFailureHandler(function(error) {
resultsDiv.textContent = '❌ エラー: ' + error.message;
})
.testEmailSend();
}
// リセット確認
function confirmReset() {
if (confirm('本当に全ての設定をリセットしますか?\n作成済みのフォームとスプレッドシートは削除されませんが、連携は解除されます。')) {
google.script.run
.withSuccessHandler(function(result) {
showAlert(result.message, 'success');
setTimeout(function() {
location.reload();
}, 1500);
})
.resetAllSettings();
}
}
// アラート表示
function showAlert(message, type) {
const alert = document.getElementById('alert');
alert.textContent = message;
alert.className = 'alert show alert-' + type;
setTimeout(function() {
alert.classList.remove('show');
}, 5000);
}
</script>
</body>
</html>
実際に生成されたReadme(取扱説明書)
# 光回線営業フロー自動化システム - 完全セットアップガイド ## 📁 ファイル構成 ``` gas_project/ ├── Code.gs # メインのGASコード └── index.html # Webアプリの画面 ``` --- ## 🚀 セットアップ手順 ### Step 1: GASプロジェクトの作成 1. [Google Apps Script](https://script.google.com/) にアクセス 2. 左上の「新しいプロジェクト」をクリック 3. プロジェクト名を「光回線営業自動化」などに変更 --- ### Step 2: コードファイルの追加 #### Code.gs(メインコード) 1. 左側の「コード.gs」をクリック 2. 既存のコードを全て削除 3. `Code.gs` の内容を貼り付け 4. 保存(Ctrl+S) #### index.html(画面) 1. 左側の「+」ボタン → 「HTML」を選択 2. ファイル名を `index` と入力(.htmlは自動で付く) 3. 既存のコードを全て削除 4. `index.html` の内容を貼り付け 5. 保存(Ctrl+S) --- ### Step 3: Webアプリとしてデプロイ 1. 右上の「デプロイ」ボタン → 「新しいデプロイ」 2. 歯車アイコン → 「ウェブアプリ」を選択 3. 以下を設定: - 説明: 「光回線営業自動化システム」 - 次のユーザーとして実行: 「自分」 - アクセスできるユーザー: 「自分のみ」 4. 「デプロイ」をクリック 5. 権限の承認(初回のみ) 6. 表示されたURLをコピー --- ### Step 4: 自動セットアップの実行 1. デプロイしたURLにアクセス 2. 各項目を入力: - 会社名 - 担当者名 - 電話番号 - メールアドレス - カレンダーID 3. 「自動セットアップを実行」をクリック 4. 完了を待つ(30秒〜1分程度) --- ## 🖥️ Webアプリの画面説明 ### セットアップ画面 ![Setup Screen] | 項目 | 説明 | |------|------| | 会社名 | メールの署名に使用 | | 担当者名 | メールの署名に使用 | | 会社電話番号 | メールの署名に使用 | | 返信用メールアドレス | 返信先として設定 | | カレンダーID | 空き確認・予定登録に使用 | | アポイント所要時間 | カレンダー登録時の長さ | | 営業時間 | この範囲外の希望は自動スキップ | ### 完了画面 セットアップ完了後は以下が表示されます: - **お問い合わせフォームURL** - チラシのQRコードに使用 - **管理スプレッドシートURL** - 予約管理用 - **フォーム編集URL** - フォームの質問を変更したい場合 - **QRコード** - そのまま印刷可能 --- ## 📊 自動作成されるもの ### Googleフォーム 以下の質問が自動で作成されます: | 質問 | 形式 | 必須 | |------|------|------| | お名前 | 短文 | ✅ | | メールアドレス | 短文 | ✅ | | 電話番号 | 短文 | ✅ | | ご住所 | 長文 | ✅ | | 第1希望日時 | 日時 | ✅ | | 第2希望日時 | 日時 | ❌ | | 第3希望日時 | 日時 | ❌ | | ご要望・備考 | 長文 | ❌ | ### スプレッドシート フォーム回答に加え、以下の列が追加されます: | 列 | 内容 | |----|------| | J列: 確定日時 | 自動で確定した日時 | | K列: ステータス | 未処理/アポ確定/リマインド送信済/完了/日程調整中/エラー | | L列: イベントID | Googleカレンダーの予定ID | | M列: 処理日時 | 処理した日時 | ### 条件付き書式 ステータス列は自動で色分けされます: - 🟢 **アポ確定** - 緑 - 🔵 **リマインド送信済** - 青 - 🟡 **日程調整中** - 黄 - 🔴 **エラー** - 赤 ### トリガー 以下のトリガーが自動設定されます: | トリガー | 実行タイミング | 処理内容 | |---------|---------------|---------| | onFormSubmit | フォーム送信時 | 空き確認→日程確定→カレンダー登録→確認メール送信 | | sendReminderEmails | 毎日11:50 | 翌日アポのリマインドメール送信 | | processUnprocessedEntries | 毎時 | 未処理データの再処理 | --- ## 🔄 自動化フロー ``` お客様がQRコードを読み取り ↓ Googleフォームに入力・送信 ↓ ━━━━ 以下すべて自動 ━━━━ ↓ スプレッドシートに記録 ↓ カレンダー空き確認 ├─ 空きあり → 日程確定 │ → カレンダーに予定登録 │ → 確認メール送信 │ → ステータス「アポ確定」 │ └─ 空きなし → 日程調整メール送信 → ステータス「日程調整中」 ↓ (前日12:00) ↓ リマインドメール自動送信 ↓ ステータス「リマインド送信済」 ``` --- ## 📧 送信されるメール ### 1. 確認メール(日程確定時) ``` 件名: 【○○株式会社】訪問日時確定のご連絡 ○○様 この度は、光回線サービスにご興味をお持ちいただき、 誠にありがとうございます。 下記の日程にて、ご訪問させていただきます。 ━━━━━━━━━━━━━━━━━━━━━━━━━━━ ■ 訪問日時 2024年12月15日(日) 14:00 (所要時間:約60分) ■ 訪問先 東京都渋谷区... ━━━━━━━━━━━━━━━━━━━━━━━━━━━ (以下署名) ``` ### 2. 日程調整メール(空きがない場合) ``` 件名: 【○○株式会社】訪問日程のご相談 ○○様 大変恐れ入りますが、ご希望いただいた日時に 先約がございました。 改めて日程を調整させていただきたく、 担当者より折り返しご連絡させていただきます。 ``` ### 3. リマインドメール(前日12:00) ``` 件名: 【明日のご予約】○○株式会社 訪問のご確認 ○○様 明日のご訪問について、リマインドのご連絡です。 ■ 訪問日時 2024年12月15日(日) 14:00 ■ 訪問先 東京都渋谷区... ``` --- ## 🔧 トラブルシューティング ### Q: フォーム送信しても何も起きない 1. **トリガーを確認** - GASエディタ → 左メニュー「トリガー」 - `onFormSubmit` があるか確認 - なければ、Webアプリから再セットアップ 2. **実行ログを確認** - GASエディタ → 左メニュー「実行数」 - エラーメッセージを確認 ### Q: カレンダーに登録されない 1. **カレンダーID確認** - 通常はGmailアドレスと同じ - 共有カレンダーの場合は設定画面でID確認 2. **テスト実行** - Webアプリの「カレンダー接続テスト」ボタンをクリック ### Q: メールが届かない 1. **迷惑メールフォルダを確認** 2. **送信制限を確認** - 無料Googleアカウント: 100通/日 - Google Workspace: 1500通/日 3. **テスト実行** - Webアプリの「メール送信テスト」ボタンをクリック ### Q: 営業時間外の希望が無視される 仕様です。営業時間設定で開始・終了時刻を調整してください。 --- ## 📝 カスタマイズ ### メール文面の変更 `Code.gs` 内の以下の関数を編集: - `sendConfirmationEmail()` - 確認メール - `sendNoAvailabilityEmail()` - 日程調整メール - `sendReminderEmail()` - リマインドメール ### フォームの質問を変更 自動生成後、Googleフォームの編集画面で直接編集できます。 ただし、**列の順序を変更した場合は `COLUMNS` 定数も修正が必要です。** ### 営業日の制限を追加 `findAvailableSlot()` 関数に曜日チェックを追加: ```javascript // 土日はスキップ const dayOfWeek = startTime.getDay(); if (dayOfWeek === 0 || dayOfWeek === 6) { Logger.log('⏭️ 土日をスキップ'); continue; } ``` --- ## 📞 サポート 不明点があれば、Webアプリの画面から各種テストを実行して状態を確認してください。

| ファイル名 | 種類 | 説明 |
|---|---|---|
| Code | GS (Google Apps Script) | メインのプログラムコード |
| Index | HTML | セットアップ用Webアプリの画面 |
| Readme | MD (Markdown) | セットアップガイド |
GASコードの機能一覧
Claudeが生成するGASコードには、以下の7つの機能が含まれています。

/**
* 光回線営業フロー 完全自動化システム
*
* 【機能】
* 1. Googleフォームを自動生成
* 2. スプレッドシートを自動フォーマット
* 3. フォーム送信時に自動でカレンダー空き確認
* 4. 最適な日程を自動確定
* 5. Googleカレンダーに予定登録
* 6. お客様に確認メール送信
* 7. 前日12時にリマインドメール送信
*/
3. セットアップ手順:3つのステップで自動化を実現
ここからは、実際にシステムをセットアップしていく手順を具体的に解説します。
ステップ1:Google Apps Script (GAS) の準備
まず、自動化の心臓部となるプログラムを設置します。
1-1. 新しいGASプロジェクトを作成
- Google Driveを開きます
- 「新規」→「その他」→「Google Apps Script」を選択
- 新しいプロジェクトが作成されます
1-2. コードを貼り付け
- Claudeが生成した
Code.gsファイルの中身をすべてコピー - GASの
コード.gsファイルに貼り付けます

- 左側のファイル一覧で「+」をクリックし、「HTML」を選択
- ファイル名を
indexに変更 - Claudeが生成した
index.htmlの中身を貼り付けます
1-3. コードの設定値を確認
コード内には、以下のようなデフォルト設定が含まれています。必要に応じて変更できます。

const DEFAULT_CONFIG = {
calendarId: '', // GoogleカレンダーID
salesPersonName: '営業担当', // 担当者名
companyName: 'NEXT INNOVATION株式会社', // 会社名
companyPhone: '03-XXXX-XXXX', // 会社電話番号
replyEmail: '', // 返信用メールアドレス
appointmentDuration: 60, // アポ所要時間(分)
businessHoursStart: 10, // 営業開始時間
businessHoursEnd: 19, // 営業終了時間
sheetName: 'フォームの回答 1' // シート名
};
1-4. ウェブアプリとしてデプロイ
- 画面右上の「デプロイ」ボタンをクリック
- 「新しいデプロイ」を選択
- 「種類の選択」で歯車アイコンをクリック
- 「ウェブアプリ」を選択

- 以下の通り設定します:
| 設定項目 | 設定値 |
|---|---|
| 説明 | 任意(例:光回線営業フロー自動化) |
| 次のユーザーとして実行 | 自分 |
| アクセスできるユーザー | 全員 |
- 「デプロイ」をクリック
- 承認を求められたら、「アクセスを承認」をクリック
- 表示されたウェブアプリURLをコピーして保存しておきます
重要: このURLは次のステップで使用します。必ずコピーしておいてください。
ステップ2:Webアプリでの初期設定
次に、先ほどデプロイしたWebアプリを使って、システムの初期設定を行います。
2-1. 設定画面にアクセス
コピーしたウェブアプリURLにアクセスすると、以下のような設定画面が表示されます。

画面上部には、セットアップの進行状況が表示されます:
1. 設定入力 → 2. 自動生成 → 3. 完了
2-2. 会社情報を入力
以下の項目を入力します:
| 項目 | 説明 | 入力例 |
|---|---|---|
| 会社名 | あなたの会社名 | NEXT INNOVATION株式会社 |
| 担当者名 | 営業担当者の名前 | 黒山結音 |
| 会社電話番号 | 連絡先電話番号 | 03-XXXX-XXXX |
| 返信用メールアドレス | お客様からの返信先 | XXXX@gmail.com |
2-3. カレンダー・営業時間を設定

| 項目 | 説明 | 入力例 |
|---|---|---|
| GoogleカレンダーID | 通常はGmailアドレスと同じ | your-email@gmail.com |
| アポイント所要時間(分) | 1回の訪問にかかる時間 | 60分(1時間) |
| シート名 | スプレッドシートのシート名 | フォームの回答 1 |
| 営業開始時間 | アポを入れられる最早時間 | 10:00 |
| 営業終了時間 | アポを入れられる最遅時間 | 19:00 |
ヒント: 共有カレンダーを使う場合は、カレンダー設定画面でIDを確認してください。
2-4. セットアップを実行
すべての入力が終わったら、「自動セットアップを実行」ボタンをクリックします。
注意: 初回実行時には、再度Googleアカウントでの承認が必要になります。画面の指示に従って承認してください。
2-5. セットアップ完了
処理が完了すると、以下のような「セットアップ完了」画面が表示されます。

この画面には、自動生成された重要なリンクが表示されます:
| 項目 | 用途 |
|---|---|
| お問い合わせフォーム | チラシのQRコードに使用するURL |
| 管理スプレッドシート | 予約状況を確認・管理する画面 |
| フォーム編集 | フォームの質問を変更したい場合に使用 |
| チラシ用QRコード | そのまま印刷してチラシに使用可能 |
また、「テスト・メンテナンス」セクションには以下のボタンがあります:
- カレンダー接続テスト: カレンダーとの連携が正常か確認
- メール送信テスト: メール送信が正常か確認
- 設定リセット: すべての設定を初期化(赤いボタン)
ステップ3:自動化の動作確認
最後に、システムが正しく動作するか確認してみましょう。
3-1. お客様役としてフォームに入力
セットアップ完了画面に表示された「お問い合わせフォーム」にアクセスします。

フォームには以下の項目が表示されます:
| 項目 | 形式 | 必須 | 説明 |
|---|---|---|---|
| お名前 | 短文 | ✅ | 例:山田 太郎 |
| メールアドレス | 短文 | ✅ | 確認メールをお送りします |
| 電話番号 | 短文 | ✅ | 例:090-1234-5678 |
| ご住所 | 長文 | ✅ | 訪問先のご住所をご記入ください |
| 第1希望日時 | 日時 | ✅ | ご希望の訪問日時をお選びください |
| 第2希望日時 | 日時 | ❌ | 第1希望が難しい場合の候補 |
| 第3希望日時 | 日時 | ❌ | 第2希望が難しい場合の候補 |
| ご要望・備考 | 長文 | ❌ | その他ご要望があればご記入ください |

テスト情報を入力して「送信」をクリックします。
3-2. スプレッドシートでデータを確認
フォームを送信すると、即座に「管理スプレッドシート」にデータが記録されます。

スプレッドシートの列構成は以下の通りです:
| 列 | 内容 |
|---|---|
| A列 | タイムスタンプ |
| B列 | お名前 |
| C列 | メールアドレス |
| D列 | 電話番号 |
| E列 | ご住所 |
| F列 | 第1希望日時 |
| G列 | 第2希望日時 |
| H列 | 第3希望日時 |
| I列 | ご要望・備考 |
| J列 | 確定日時(自動入力) |
| K列 | ステータス(自動更新) |
| L列 | カレンダーイベントID(自動入力) |
| M列 | 処理日時(自動入力) |
ステータスの種類:
| ステータス | 意味 |
|---|---|
| 未処理 | フォーム送信直後 |
| アポ確定 | 日程が確定し、カレンダー登録・メール送信完了 |
| リマインド送信済 | 前日リマインダーメール送信完了 |
| 完了 | アポイント終了 |
| 日程調整中 | 希望日時がすべて埋まっている場合 |
| エラー | 処理中にエラーが発生 |
3-3. 確認メールを受信
アポイントが確定すると、フォームに入力したメールアドレス宛に、確定日時が記載された確認メールが自動で届きます。

メールの内容例:
テスト太郎 様
この度は、光回線サービスにご興味をお持ちいただき、
誠にありがとうございます。下記の日程にて、ご訪問させていただきます。
■ 訪問日時
2026年1月8日(木) 11:00
(所要時間:約60分)■ 訪問先
テスト当日は、現在のインターネット環境についてお伺いし、
最適なプランをご提案させていただきます。ご不明な点やご変更がございましたら、
お気軽にご連絡ください。
NEXT INNOVATION株式会社
担当: 黒山結音
TEL: 03-XXXX-XXXXEmail: yuitokuroyama@gmail.com
3-4. カレンダーに予定が登録される
ご自身のGoogleカレンダーを確認すると、確定したアポイントが自動で登録されています。

予定の詳細には、以下の情報が自動で記載されています:
- タイトル: 【光回線】テスト太郎様 訪問アポ
- 日時: 1月 8日 (木曜日) ・ 午前11:00〜午後12:00
- 場所: テスト(お客様のご住所)
- お客様情報: お名前、メール、電話番号、ご住所
- ご要望・備考: お客様が入力した内容
- 対応メモ: (ここに対応内容を記録)
- 通知: 1時間前、1日前
4. 自動生成されるもの一覧
セットアップを実行すると、以下のものが自動で生成されます。
Googleフォーム
お客様が情報を入力するためのフォームが自動生成されます。
| 質問 | 形式 | 必須 |
|---|---|---|
| お名前 | 短文 | ✅ |
| メールアドレス | 短文 | ✅ |
| 電話番号 | 短文 | ✅ |
| ご住所 | 長文 | ✅ |
| 第1希望日時 | 日時 | ✅ |
| 第2希望日時 | 日時 | ❌ |
| 第3希望日時 | 日時 | ❌ |
| ご要望・備考 | 長文 | ❌ |
スプレッドシート
フォームの回答を管理するスプレッドシートが自動生成されます。
- フォーム回答列(A〜I列)
- 追加管理列(J〜M列):確定日時、ステータス、カレンダーイベントID、処理日時
QRコード
チラシに印刷できるQRコードが自動生成されます。
トリガー(自動実行設定)
- フォーム送信時トリガー:フォームが送信されると自動で処理を開始
- 時間ベーストリガー:毎日12時にリマインダーメールを送信
5. カスタマイズ方法
システムをカスタマイズしたい場合は、以下の箇所を編集します。
アポ所要時間の変更
コード内のAPPOINTMENT_DURATION_MINUTESの値を変更します。
appointmentDuration: 60, // 60分 → 90分に変更する場合は90に
メール文面の変更
コード内のsendConfirmationEmail()関数とsendReminderEmail()関数内のテンプレートを編集します。
営業時間の変更
コード内のbusinessHoursStartとbusinessHoursEndの値を変更します。
businessHoursStart: 10, // 営業開始時間(24時間表記)
businessHoursEnd: 19, // 営業終了時間(24時間表記)
6. まとめ:AIと共に創る、新しい働き方へ
この記事で紹介した方法は、AIとGASという強力なツールを組み合わせることで、これまで手作業で行っていた定型業務を劇的に効率化できることを示しています。
このシステムで得られるメリット
| メリット | 詳細 |
|---|---|
| 時間の節約 | 日程調整に費やしていた時間をゼロに |
| ミスの削減 | 人的ミスによるダブルブッキングを防止 |
| 顧客満足度向上 | 迅速な対応で顧客体験を向上 |
| スケーラビリティ | 問い合わせ数が増えても対応可能 |
重要なポイント
重要なのは、「何を自動化したいか」を明確にイメージし、それをAIに伝えることです。プログラミングの専門知識がなくても、アイデア次第で誰もがこのような強力なシステムを構築できる時代が到来しました。
ぜひ、あなたもこの記事を参考に、自分だけの業務自動化システムを構築してみてください。そして、AIによって生み出された時間で、より創造的で価値のある仕事に挑戦してみてはいかがでしょうか。
お疲れ様でした! これで、お問い合わせから日程調整、メール通知までの一連のフローが完全に自動化されました。

コメント