はじめに
今日チェックインする宿泊者と、チェックアウトする客室だけを確認したいのに、毎朝ねっぱんの管理画面を開くのは少し手間です。
筆者が運営する客室4室の宿では、ねっぱんの予約をGoogleカレンダーへ自動登録しています。今回は、そのカレンダーから当日の予約だけを取り出し、毎朝メールで受け取る仕組みを作りました。
この記事では、コピペで使えるコードと設定方法を紹介します。
この仕組みで毎朝確認できること
今回作る仕組みでは、Googleカレンダーに入っている予約から、次の情報を自動で取り出します。
- 今日チェックアウトする予約
- 今日チェックインする予約
- チェックアウト後に同じ客室へ予約が入る当日入れ替え
- 予約サイト
- 決済方法
朝に届くメールは、次のような内容です。
当日入れ替えの客室を先に表示するため、どの部屋から清掃すればよいかを判断しやすくなります。
2026年8月14日の客室状況
【優先清掃・当日入れ替え】
■ シングル
OUT:山田太郎様
IN :佐藤花子様(Booking.com/事前決済)
【チェックアウトのみ】
・ツイン:鈴木一郎様
【チェックインのみ】
・シングルツイン:高橋花子様(じゃらん/現地決済)
-----
チェックアウト:2件
チェックイン:2件
正式な予約管理は、これまでどおりねっぱんで行います。
この通知は、朝の段取りを決めるための一覧として使います。予約の変更やキャンセルがあった場合は、Googleカレンダー側も修正してください。
事前にGoogleカレンダーへ予約を登録する
この記事のコードは、ねっぱんの予約通知メールからGoogleカレンダーへ登録した予定を使います。
まだ予約を自動登録していない場合は、先に次の記事の設定を行ってください。
neppan-mail-to-google-calendar
前の記事のコードでは、カレンダーの予定が次の形式で登録されます。
予定名: 【シングル】山田太郎様
説明:
決済方法:事前決済
予約サイト:Booking.com
宿泊者氏名:山田太郎
部屋タイプ:Single Room with Shared Shower and Toilet
今回のコードは、説明欄にある「宿泊者氏名」と「部屋タイプ」を目印にして、予約の予定だけを取得します。
そのため、同じカレンダーに清掃予定や私用の予定が入っていても、それらは通知の対象になりません。
コードを貼り付けて2か所を書き換える
前の記事で作成したGoogle Apps Scriptのプロジェクトを開き、予約登録用コードの下に次のコードを追加してください。
新しいプロジェクトへ貼り付けても動きますが、前の記事と同じプロジェクトにまとめた方が管理しやすくなります。
function sendTodayRoomStatus() {
// ▼▼▼ 設定 ▼▼▼
const calendarId = 'カレンダーID';
const notificationEmail = 'あなたのメールアドレス';
// OTAごとの部屋タイプ名を、共通の部屋名にまとめる
//
const roomRules = [
{
name: 'シングル',
keywords: [
'Single Room with Shared Shower and Toilet',
'シングルルーム',
``,
``,
``
]
},
{
name: 'ツイン',
keywords: [
'Twin Room',
'ツインルーム',
'ツイン',
``,
``,
``
]
},
{
name: 'メールに表示する名前',
keywords: [
'カレンダーに入ってる部屋タイプ名①',
'カレンダーに入ってる部屋タイプ名②',
``,
``,
``
]
}
];
// ▲▲▲ 設定ここまで ▲▲▲
const calendar = CalendarApp.getCalendarById(calendarId);
if (!calendar) {
throw new Error(`カレンダーが見つかりません: ${calendarId}`);
}
const timeZone = Session.getScriptTimeZone();
const today = new Date();
today.setHours(0, 0, 0, 0);
// 今日チェックアウトする予定を拾うため、前日から翌日まで取得
const searchStart = new Date(today);
searchStart.setDate(searchStart.getDate() - 1);
const searchEnd = new Date(today);
searchEnd.setDate(searchEnd.getDate() + 2);
const events = calendar.getEvents(searchStart, searchEnd);
const checkins = [];
const checkouts = [];
for (const event of events) {
//if (!event.isAllDayEvent()) {
// continue;
//}
const startDate = event.getStartTime();
const endDate = event.getEndTime();
const title = event.getTitle();
const description = event.getDescription();
const roomType = getDescriptionValue(description, '部屋タイプ');
const ota = getDescriptionValue(description, '予約サイト');
const paymentMethod = getDescriptionValue(description, '決済方法');
const guestName =
getDescriptionValue(description, '宿泊者氏名') ||
getGuestNameFromTitle(title);
const normalizedRoomName = normalizeRoomName(roomType, roomRules);
const reservation = {
roomName: normalizedRoomName,
rawRoomType: roomType,
guestName,
ota,
paymentMethod
};
if (isSameDate(startDate, today, timeZone)) {
checkins.push(reservation);
}
if (isSameDate(endDate, today, timeZone)) {
checkouts.push(reservation);
}
}
const result = classifyRooms(checkins, checkouts);
const subject = `【客室状況】${formatDate(today, timeZone)}`;
const body = createNotificationBody(
today,
result,
checkins,
checkouts,
timeZone
);
//GmailApp.sendEmail(notificationEmail, subject, body);
Logger.log(body);
}
/**
* 説明欄から「部屋タイプ:○○」などの値を取得
*/
function getDescriptionValue(description, label) {
if (!description) {
return '';
}
const pattern = new RegExp(`${escapeRegExp(label)}\\s*:\\s*(.*)`);
const match = description.match(pattern);
return match ? match[1].trim() : '';
}
/**
* OTAごとの部屋名を共通名へ変換
*/
function normalizeRoomName(rawRoomType, roomRules) {
if (!rawRoomType) {
return '部屋タイプ不明';
}
const lowerRoomType = rawRoomType.toLowerCase();
for (const rule of roomRules) {
const matched = rule.keywords.some(keyword =>
lowerRoomType.includes(keyword.toLowerCase())
);
if (matched) {
return rule.name;
}
}
// 対応表にない部屋名は、そのまま表示
return rawRoomType;
}
/**
* OUTとINを部屋名で照合
*/
function classifyRooms(checkins, checkouts) {
const checkinsByRoom = groupByRoom(checkins);
const checkoutsByRoom = groupByRoom(checkouts);
const roomNames = new Set([
...Object.keys(checkinsByRoom),
...Object.keys(checkoutsByRoom)
]);
const turnovers = [];
const checkoutOnly = [];
const checkinOnly = [];
for (const roomName of roomNames) {
const incoming = checkinsByRoom[roomName] || [];
const outgoing = checkoutsByRoom[roomName] || [];
if (incoming.length > 0 && outgoing.length > 0) {
turnovers.push({
roomName,
incoming,
outgoing
});
} else if (outgoing.length > 0) {
checkoutOnly.push({
roomName,
reservations: outgoing
});
} else if (incoming.length > 0) {
checkinOnly.push({
roomName,
reservations: incoming
});
}
}
return {
turnovers,
checkoutOnly,
checkinOnly
};
}
function groupByRoom(reservations) {
return reservations.reduce((groups, reservation) => {
const roomName = reservation.roomName;
if (!groups[roomName]) {
groups[roomName] = [];
}
groups[roomName].push(reservation);
return groups;
}, {});
}
/**
* 通知本文を作成
*/
function createNotificationBody(
today,
result,
checkins,
checkouts,
timeZone
) {
const lines = [];
lines.push(`${formatDate(today, timeZone)}の客室状況`);
lines.push('');
lines.push('【優先清掃・当日入れ替え】');
if (result.turnovers.length === 0) {
lines.push('なし');
} else {
for (const room of result.turnovers) {
lines.push(`■ ${room.roomName}`);
for (const reservation of room.outgoing) {
lines.push(` OUT:${reservation.guestName || '氏名不明'}様`);
}
for (const reservation of room.incoming) {
lines.push(
` IN :${reservation.guestName || '氏名不明'}様` +
formatSupplement(reservation)
);
}
}
}
lines.push('');
lines.push('【チェックアウトのみ】');
if (result.checkoutOnly.length === 0) {
lines.push('なし');
} else {
for (const room of result.checkoutOnly) {
for (const reservation of room.reservations) {
lines.push(
`・${room.roomName}:${reservation.guestName || '氏名不明'}様`
);
}
}
}
lines.push('');
lines.push('【チェックインのみ】');
if (result.checkinOnly.length === 0) {
lines.push('なし');
} else {
for (const room of result.checkinOnly) {
for (const reservation of room.reservations) {
lines.push(
`・${room.roomName}:${reservation.guestName || '氏名不明'}様` +
formatSupplement(reservation)
);
}
}
}
lines.push('');
lines.push('-----');
lines.push(`チェックアウト:${checkouts.length}件`);
lines.push(`チェックイン:${checkins.length}件`);
lines.push('');
lines.push(`その他直接予約や別カレンダーの予約も確認してください。`)
return lines.join('\n');
}
function formatSupplement(reservation) {
const details = [];
if (reservation.ota) {
details.push(reservation.ota);
}
if (reservation.paymentMethod) {
details.push(reservation.paymentMethod);
}
return details.length > 0 ? `(${details.join('/')})` : '';
}
function getGuestNameFromTitle(title) {
if (!title) {
return '';
}
return title
.replace(/^【.*?】/, '')
.replace(/様$/, '')
.trim();
}
function isSameDate(date1, date2, timeZone) {
return (
Utilities.formatDate(date1, timeZone, 'yyyy-MM-dd') ===
Utilities.formatDate(date2, timeZone, 'yyyy-MM-dd')
);
}
function formatDate(date, timeZone) {
return Utilities.formatDate(date, timeZone, 'yyyy年M月d日');
}
function escapeRegExp(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
基本的に書き換えるのは、カレンダーIDと通知先メールアドレスの2か所です。
カレンダーIDは、Googleカレンダーの「設定と共有」を開き、「カレンダーの統合」にあるカレンダーIDをコピーします。
前の記事と同じGoogle Apps Scriptへ追加する場合は、同じカレンダーIDを使ってください。
手動で実行して通知を確認する
コードを追加したら、画面上部の関数一覧から「sendTodayRoomStatus」を選び、「実行」を押します。
初回は、Googleカレンダーの読み取りとメール送信に関するアクセス許可を求められます。画面の案内に沿って許可してください。
実行後、設定したメールアドレスに当日の予約一覧が届けば成功です。
当日の予約がない場合も、チェックインとチェックアウトが0件のメールが届きます。
テスト用の予定を作る場合は、予約を登録しているカレンダーに次の2件を追加します。
- 昨日から今日までのチェックアウト予定
- 今日から明日までのチェックイン予定
予定名と説明欄は、前の記事で自動登録される予約と同じ形式にしてください。
同じ客室名を付けると、「優先清掃・当日入れ替え」に表示されます。
毎朝自動で届くように設定する
動作を確認したら、Google Apps Scriptのトリガーを設定します。
- 左側にある時計マークの「トリガー」を開く
- 右下の「トリガーを追加」を押す
- 実行する関数で「sendTodayRoomStatus」を選ぶ
- イベントのソースで「時間主導型」を選ぶ
- 時間ベースのトリガーで「日付ベースのタイマー」を選ぶ
- 通知を受け取りたい時間帯を選ぶ
筆者の宿では、清掃やチェックイン準備を始める前に確認できるよう、朝に通知する設定を使っています。
時間主導型トリガーは、指定した時間帯の中で実行されます。毎日8時ちょうどではなく、8時台のいずれかの時間に届く場合があります。
終日予定を時間付き予定に変更しても取得できる
前の記事では、チェックイン日からチェックアウト日までを終日予定としてGoogleカレンダーへ登録しています。
ただし、実際の運用では次のような変更をすることがあります。
- 到着予定を15時に変更する
- 遅いチェックインだけ時刻を入れる
- チェックアウト時刻を10時に設定する
今回のコードは、終日予定だけに限定していません。
予定の開始日をチェックイン日、終了日をチェックアウト日として判断するため、手動で時間付き予定へ変更しても通知されます。
なお、日付そのものを変更すると、変更後の日付で判定されます。
当日入れ替えを正しく判定するための注意点
当日入れ替えは、予定名の先頭にある客室名で判断します。
例えば、次の2件は同じ「シングル」として扱われます。
【シングル】山田太郎様 【シングル】佐藤花子様
一方で、次の2件は別の客室として扱われます。
【シングル】山田太郎様 【Single Room】佐藤花子様
OTAごとに異なる部屋タイプ名を使っている場合は、前の記事の予約登録コードで、カレンダー上の客室名を同じ表記にそろえてください。
また、同じ「シングル」という部屋タイプが2室以上ある施設では、部屋タイプだけで物理的に同じ客室かを判断できません。
その場合は、次のように各客室を区別する必要があります。
- シングルA
- シングルB
- 101号室
- 102号室
客室を区別できない場合でも、今日のチェックインとチェックアウトの一覧は確認できます。ただし、当日入れ替えの判定は参考情報として扱ってください。
運用上の注意点
この仕組みは、Googleカレンダーに登録されている内容をもとに通知します。
ねっぱん側で予約が変更またはキャンセルされても、Googleカレンダーの予定が自動修正されるわけではありません。
変更やキャンセルが発生した場合は、カレンダー側の予定も手動で修正してください。
また、メールには宿泊者氏名や決済方法が含まれます。スタッフへ転送する場合は、通知先と共有範囲を確認しておきます。
この通知だけを正式な予約台帳として使わず、詳細を確認するときはねっぱんの管理画面を開いてください。
まとめ
ねっぱんの当日チェックインとチェックアウトを毎朝通知する方法を整理します。
- Googleカレンダーの予約から当日の開始・終了予定を取得する
- チェックイン、チェックアウト、当日入れ替えに分けて表示する
- 終日予定を時間付き予定に変更しても取得できる
- 客室名を統一すると当日入れ替えを判定できる
- 正式な予約情報はねっぱんで確認する
ねっぱんの管理画面を開かなくても、その日の清掃と受け入れ準備に必要な情報を朝のメールで確認できるようになります。
宿泊者の声を、口コミに変える仕組みを
無料で試してみませんか
QRコードを設置するだけで、ゲストの感想がGoogle口コミへ自然につながります。
先着10施設限定、3ヶ月間は全機能を無料でご利用いただけます。
先着10施設限定・3ヶ月無料
