路上弁当の予約受付をLINEで自動化する

LINE公式アカウント × エルメ

路上弁当の予約受付をLINEで自動化する

友だち追加から予約フォーム・台帳への自動取込・前日リマインドまで。手を動かせば同じ仕組みが作れます。

1. 全体設計とスプレッドシートの準備

路上での弁当販売は「今日はどこに出ているのか」「売り切れる前に取り置きしたい」という問い合わせが電話とSNSに分散しがちです。ここではその受付をLINEに一本化し、注文内容がそのままスプレッドシートの台帳に積み上がる仕組みを作ります。

役割分担(ここを間違えると動かない)

LINE公式アカウントの Webhook(外部へ通知を送る先)は1つしか設定できません。エルメを使う時点でその1枠はエルメが占有します。したがってGASに「LINEからの受信」をさせることはできず、お客様個人へのLINE送信もGASには任せられません(ユーザーIDを取る手段がないため)。

コンポーネント担当この記事での役割
エルメ受信・自動応答・フォーム・リマインド配信・タグ付与予約フォーム(章3)、前日リマインド(章5)、一斉配信・リッチメニュー(章6)
連携シートエルメがフォーム回答を自動記録回答の受け口(人は触らない)
GAS(時間トリガー専用)受信も個人への送信もしない連携シート→台帳の取込・採番・単価付与、店主へのメール(章4)
スプレッドシート注文の保存と集計注文台帳・メニューマスタ
重要:章4で作るGASは、エルメがスプレッドシートに書いたフォーム回答を時間になったら読みに行くだけです。LINE本体のWebhookはエルメが持ったままなので、両者は問題なく共存します。GASにLINE Messaging APIの受信や、お客様個人へのpushを担わせる設計にすると、その時点でエルメの接続が壊れます。

スプレッドシートを2枚用意する

エルメの連携シート(章3で自動生成)とは別に、人が使うシートを2枚作ります。ここで決めた列名と列順は、以降のGASコード・本文の説明・図表すべてで一致させます。後から列を挿入するとGASの列番号がずれるので、先に確定させてください。

シート①:注文台帳

列列名型備考
A注文ID文字列GASが採番(例 B261011-001)
B受付日時日時連携シートの回答日時をGASが写す
CLINE表示名文字列連携シートからGASが写す
D受取日日付フォームの日付回答をGASが写す
E受取場所文字列販売場所名
Fメニュー名文字列メニューマスタと一致
G単価数値GASがマスタから引く
H個数数値フォームの回答(1〜3)
I金額数値G × H をGASが計算
Jステータス列挙受付/受渡済/キャンセル(店主が更新)
K取込キー文字列GASが記入(同じ回答を二重に写さないため)

シート②:メニューマスタ

列列名例
Aメニューコードhigawari
Bメニュー名日替わり弁当
C単価550
D販売状況販売中/休止
なぜ価格をマスタに置くのか:フォームの選択肢に価格を埋め込むと、値上げのたびにフォームと案内文を直すことになります。GASがマスタのB列(メニュー名)で突き合わせて単価を引く形にしておけば、値段の変更はスプレッドシートの1マスだけで済みます。章3のフォームの選択肢は、このB列と一字一句同じにします。

タグの命名規則を1つ決める

エルメのタグはカテゴリ_状態の形に統一します。表記が揺れると絞り込み配信が効かなくなるため、この記事では以下の5つだけを使います。

タグ名付与タイミング用途
友だち_新規友だち追加時(章2)初回案内の配信対象
流入_店頭販売台のQRから追加設置場所ごとの効果測定
流入_チラシチラシのQRから追加チラシ配布の効果測定
予約_確定フォーム回答時(章3)一度でも予約した人(リピーター向け配信の絞り込み)
予約_キャンセルキャンセル申告時(章7)前日リマインドを止める条件。次の予約で自動的に外れる
履歴のタグは外さない:友だち_新規や予約_確定は、あとから何があっても外しません。外すと「登録したが一度も注文していない人」「注文したことがある人」の絞り込みができなくなるためです。外したり付けたりするのは、状態を表す予約_キャンセルだけです。
◆ ◆ ◆

2. 友だち追加とウェルカムメッセージ

QRコードを読み取って友だち追加した瞬間に、あいさつメッセージが自動で届くようにします。エルメの「流入経路」を使うと、どのQRコードから登録されたかをタグで区別できます。販売台のPOPとチラシ、どちらが効いているかが数字で見えるようになります。

流入経路をつくる

操作場所:エルメ管理画面 > 友だち管理 > 流入経路

  1. 「新規作成」を押し、流入経路名に「販売台POP」と入力する
  2. 「友だち追加時に付与するタグ」に 友だち_新規 と 流入_店頭 を指定する
  3. 「友だち追加時のメッセージ」に、下のテンプレを貼り付ける
  4. 保存すると専用のQRコードとURLが発行されるので、QRコードを画像でダウンロードする
  5. 同じ手順をもう一度行い、名前「チラシ」・タグ 友だち_新規/流入_チラシ で2本目を作る
  6. 販売台のPOPには1本目、配布チラシには2本目のQRを印刷して使い分ける
QRコードは屋外で1メートル離れて読める大きさが目安です。A5用紙に一辺8cm以上で印刷し、雨天を考えてラミネート加工しておくと安心です。

コピペ用テンプレ(ウェルカムメッセージ)

友だち追加ありがとうございます! 路上弁当「まんぷく亭」です。 このLINEでできること ・今日の販売場所と時間の確認 ・お弁当の取り置き予約 ・売り切れ/臨時休業のお知らせ 下のメニューから「予約する」を押すと、 30秒で取り置きのご予約ができます。 本日もお待ちしています。
ウェルカムメッセージは友だち追加時に1回だけ届きます。スマホでは4〜5行を超えると折りたたまれるため、「何ができるか」と「次に押すボタン」の2点に絞ってください。
(QRコードを読み取って友だち追加)
友だち追加ありがとうございます!路上弁当「まんぷく亭」です。下のメニューから「予約する」を押すと、30秒で取り置きのご予約ができます。
◆ ◆ ◆

3. 注文受付フォーム(受取日・場所・メニュー)

エルメのフォームを使い、1画面で予約が完了する入口を作ります。フォームにした理由は2つあります。回答がGoogleスプレッドシートに自動で記録されること(章4)、そして受取日を「日付」として受け取れるので、エルメのリマインド配信がそのまま使えること(章5)です。

受付の締切を先に決める

ここを曖昧にすると、章5のリマインドが成立しなくなります。締切とリマインド時刻はセットで設計します。

受取の種類受付締切お知らせ
当日受取当日 10:30 までフォーム回答後の確認メッセージのみ(販売まで数時間なのでリマインドは送らない)
翌日受取前日 20:00 まで前日 20:30 にリマインド配信(章5)
時系列の検算:翌日受取の締切(前日20:00)はリマインド(前日20:30)より前です。この順序が逆になると「締切内に予約したのにリマインドが来ない」人が出ます。締切を動かすときは、リマインド時刻も一緒に動かしてください。
締切はフォームでは強制できません。フォームは時刻で受付を閉じられないため、締切は案内文(章6)と回答後メッセージで伝える運用になります。締切後に入った翌日分は、章4のGASが「受取日が明後日以降、または受付締切後」として店主に知らせます。

フォームを作る

操作場所:エルメ管理画面 > フォーム > フォーム作成

  1. 「新規作成」を押し、フォーム名を「お弁当予約」にする
  2. 質問1「受取日」を日付形式で作り、リマインド配信の基準にする設定(リマインド形式)をオンにする。説明文に「本日または明日をお選びください」と書く
  3. 質問2「受取場所」を選択形式で作り、販売場所を選択肢に並べる
  4. 質問3「お弁当」を選択形式で作り、選択肢を章1のメニューマスタのメニュー名と一字一句同じにする
  5. 質問4「個数」を選択形式で作り、「1」「2」「3」を選択肢にする
  6. 回答後のアクションに「タグ付与:予約_確定」と「タグ解除:予約_キャンセル」を設定する
  7. 回答後のメッセージに、下のテンプレを貼る
  8. 保存したら、フォーム一覧の「スプレッドシート」からGoogleアカウントを連携し、回答が記録されるシートを確認する(章4で使う)
選択肢の表記をマスタと揃える理由:章4のGASは、お客様が選んだ「お弁当」の文字列をメニューマスタのB列と突き合わせて単価を引きます。「から揚げ弁当」と「唐揚げ弁当」のように一文字でも違うと単価が引けず、店主への確認通知に回ります。
受取日を日付形式にする理由:「本日/明日」の2択ボタンにすると、エルメのリマインド配信が基準にできる日付になりません。日付形式で受け取れば、お客様が自分で日付を選ぶだけで前日リマインドの基準が決まります。

コピペ用テンプレ(回答後メッセージ)

ご予約を承りました。 受取日 :{受取日} 受取場所:{受取場所} お弁当 :{お弁当} × {個数} 明日以降の受け取りは、前日の夜に LINEでお知らせします。 変更・キャンセルは下のメニューから 「予約の確認」を押してください。
{ } の部分はフォーム回答の差し込みです。エルメの差し込みボタンから、フォームの各質問を選んで挿入してください。
(リッチメニューの「予約する」をタップ)
お弁当予約フォームはこちら → https://…
(フォームで 受取日:10/11/中央公園前/から揚げ弁当/1 を送信)
ご予約を承りました。受取日:2026/10/11/受取場所:中央公園前/お弁当:から揚げ弁当 × 1
◆ ◆ ◆

4. 注文をスプレッドシートの台帳に自動で積む

章3のフォーム回答は、エルメの連携でスプレッドシートにリアルタイムで記録されます。ただしその「連携シート」はエルメが書き込む受け口なので、人は手を触れません。GASが5分おきに新しい回答を拾い、採番と単価の付与をして注文台帳に移します。このGASは受信をしません。時間が来たらシートを読んで写すだけなので、エルメのWebhookとは無関係です。

シートの役割分担

シート誰が書くか役割
連携シート(エルメが作る)エルメだけフォーム回答の受け口。人は編集しない
注文台帳GASと店主採番・単価・ステータスを持つ正本
メニューマスタ店主メニュー名と単価
連携シートを台帳にしない理由:エルメ側の仕様で列が増えたり、回答が追記されたりしても困らないよう、人が編集する台帳は別に持ちます。連携シートは「見出し名」で読むので、列の順番が変わっても動きます。

GASを設置する

  1. 章3で連携したスプレッドシート(連携シートが入っているファイル)を開く
  2. 同じファイルに、章1の 注文台帳 と メニューマスタ のシートを作る(別ファイルでもよいが、同じファイルにすると設定が1つ減る)
  3. 拡張機能 > Apps Script を開き、エディタの中身をすべて消して下のコードを貼り付ける
  4. 冒頭の SHEET_LINKED を、エルメが作った連携シートのシート名に書き換える
  5. 冒頭の OWNER_MAIL を、店主の通知先メールアドレスに書き換える
  6. 関数 importOrders を一度手動で実行し、権限の確認画面で許可する
  7. 左メニューの時計アイコン(トリガー)から importOrders を「時間主導型 > 分ベースのタイマー > 5分おき」で登録する
  8. 同じ画面で morningReport を「日付ベースのタイマー > 午前10時〜11時」で登録する

GASコード(取込・採番・朝の準備数通知)

// ===== 路上弁当 注文取込GAS(時間トリガー専用・受信しない) =====
// 連携シート(エルメが書く)→ 注文台帳(人が使う)へ新しい回答を写す。
// LINEのWebhookはエルメが占有したままなので、このGASは受信を担当しない。

const SHEET_LINKED = 'お弁当予約';        // エルメが作った連携シートの名前に合わせる
const SHEET_ORDER  = '注文台帳';
const SHEET_MENU   = 'メニューマスタ';
const OWNER_MAIL   = 'owner@example.com'; // 店主の通知先

// 連携シートの見出し名(フォームの質問名と一致させる)
const H = { AT: '回答日時', NAME: '名前', DAY: '受取日', PLACE: '受取場所', MENU: 'お弁当', QTY: '個数' };

// 注文台帳の列番号(章1の表と一致させること)
const COL = { ID: 1, RECEIVED_AT: 2, NAME: 3, PICKUP_DAY: 4, PLACE: 5, MENU: 6,
              UNIT_PRICE: 7, QTY: 8, AMOUNT: 9, STATUS: 10, KEY: 11 };

// 5分おき:連携シートの未取込行を台帳へ
function importOrders() {
  const ss     = SpreadsheetApp.getActiveSpreadsheet();
  const linked = ss.getSheetByName(SHEET_LINKED);
  const order  = ss.getSheetByName(SHEET_ORDER);
  if (!linked || linked.getLastRow() < 2) return;

  const data = linked.getDataRange().getValues();
  const head = data[0].map(String);
  const idx  = function (name) { return head.indexOf(name); };
  const c = { at: idx(H.AT), name: idx(H.NAME), day: idx(H.DAY), place: idx(H.PLACE), menu: idx(H.MENU), qty: idx(H.QTY) };
  if (Object.keys(c).some(function (k) { return c[k] < 0; })) {
    mail('【要確認】連携シートの見出しが見つかりません', '見出し: ' + head.join(' / '));
    return;
  }

  const done  = existingKeys(order);
  const menus = loadMenus(ss);
  const alerts = [];

  for (let i = 1; i < data.length; i++) {
    const r   = data[i];
    const key = ymdhms(r[c.at]) + '|' + r[c.name] + '|' + ymd(r[c.day]) + '|' + r[c.menu];
    if (!r[c.at] || done[key]) continue;        // 空行・取込済みは飛ばす

    const menu  = menus[String(r[c.menu]).trim()];
    const qty   = Number(r[c.qty] || 1);
    const price = menu ? menu.price : '';
    const amt   = menu ? menu.price * qty : '';

    order.appendRow([
      makeOrderId(order),      // A 注文ID
      new Date(r[c.at]),       // B 受付日時
      r[c.name],               // C LINE表示名
      new Date(r[c.day]),      // D 受取日
      r[c.place],              // E 受取場所
      r[c.menu],               // F メニュー名
      price,                   // G 単価
      qty,                     // H 個数
      amt,                     // I 金額
      '受付',                  // J ステータス
      key                      // K 取込キー(重複防止)
    ]);
    done[key] = true;

    if (!menu) alerts.push('メニュー名がマスタにありません: ' + r[c.menu] + '(' + r[c.name] + ')');
    const warn = checkDeadline(new Date(r[c.at]), new Date(r[c.day]));
    if (warn) alerts.push(warn + '(' + r[c.name] + ' / ' + ymd(r[c.day]) + ')');
  }
  if (alerts.length) mail('【要確認】お弁当予約の取込', alerts.join('\n'));
}

// 締切チェック:当日は10:30まで、翌日は前日20:00まで、明後日以降は受けない
function checkDeadline(receivedAt, pickupDay) {
  const diff = Math.round((startOfDay(pickupDay) - startOfDay(receivedAt)) / 86400000);
  const hm   = receivedAt.getHours() * 100 + receivedAt.getMinutes();
  if (diff < 0)  return '受取日が過去の日付です';
  if (diff > 1)  return '受取日が明後日以降です';
  if (diff === 0 && hm > 1030) return '当日受取の締切(10:30)を過ぎた予約です';
  if (diff === 1 && hm > 2000) return '翌日受取の締切(20:00)を過ぎた予約です(前日リマインドは届きません)';
  return '';
}

// 毎朝10時台:当日ぶんの準備数を店主へメール
function morningReport() {
  const order = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_ORDER);
  const last  = order.getLastRow();
  if (last < 2) return;
  const rows  = order.getRange(2, 1, last - 1, COL.KEY).getValues();
  const today = ymd(new Date());
  const sum   = {};
  let total = 0;
  rows.forEach(function (r) {
    if (ymd(r[COL.PICKUP_DAY - 1]) !== today) return;
    if (r[COL.STATUS - 1] !== '受付') return;
    const q = Number(r[COL.QTY - 1] || 0);
    sum[r[COL.MENU - 1]] = (sum[r[COL.MENU - 1]] || 0) + q;
    total += q;
  });
  const lines = Object.keys(sum).map(function (m) { return m + ':' + sum[m] + '個'; });
  mail('本日の予約 ' + total + '個(' + today + ')', lines.join('\n') || '本日の予約はありません');
}

function loadMenus(ss) {
  const rows = ss.getSheetByName(SHEET_MENU).getDataRange().getValues();
  const map = {};
  for (let i = 1; i < rows.length; i++) {
    if (rows[i][3] === '販売中') map[String(rows[i][1]).trim()] = { price: Number(rows[i][2]) };
  }
  return map;
}

function existingKeys(sheet) {
  const last = sheet.getLastRow();
  const map = {};
  if (last < 2) return map;
  sheet.getRange(2, COL.KEY, last - 1, 1).getValues().forEach(function (r) { if (r[0]) map[r[0]] = true; });
  return map;
}

// 注文IDを採番(例 B261011-001)
function makeOrderId(sheet) {
  const prefix = 'B' + Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyMMdd') + '-';
  const last   = sheet.getLastRow();
  let seq = 0;
  if (last >= 2) {
    sheet.getRange(2, COL.ID, last - 1, 1).getValues().forEach(function (r) {
      const v = String(r[0]);
      if (v.indexOf(prefix) === 0) seq = Math.max(seq, Number(v.slice(prefix.length)));
    });
  }
  return prefix + ('00' + (seq + 1)).slice(-3);
}

function mail(subject, body) { MailApp.sendEmail(OWNER_MAIL, subject, body); }
function startOfDay(d) { const x = new Date(d); x.setHours(0, 0, 0, 0); return x; }
function ymd(d)    { return d ? Utilities.formatDate(new Date(d), 'Asia/Tokyo', 'yyyy/MM/dd') : ''; }
function ymdhms(d) { return d ? Utilities.formatDate(new Date(d), 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss') : ''; }
連携シートの見出し名は実物で確認してください。コード冒頭の H に書いた「回答日時」「名前」などは、エルメが作る連携シートの1行目に合わせます。質問名を変えたら、ここも同じに変えます。見出しが見つからないときは取り込まずに店主へメールが届くので、黙って止まることはありません。
D列(受取日)はGASが日付型で書き込みます。章7の集計式がそのまま使えます。表示形式が「自動」だと日時に見えることがあるので、D列の表示形式を「日付」にしておくと見やすくなります。
◆ ◆ ◆

5. 前日リマインド(エルメのリマインド配信)

取り置きの一番の悩みは「予約したまま来ない」ことです。前日の夜にひと言届くだけで、取りに来る率は目に見えて変わります。送るのはGASではなくエルメのリマインド配信です。章3のフォームで受取日を「リマインド形式」にしてあるので、その日付から逆算して自動で送られます。

GASに個人へのLINE送信をさせない理由

エルメを接続していると、LINEのWebhookはエルメが占有します。その状態でGASがお客様個人にpushを送るには、LINEのユーザーIDをGASが知っている必要がありますが、Webhookを持たないGASにはそれを取る手段がありません。無理に取ろうとする設計は、たいていエルメの接続を壊します。個人へ送る役はエルメ、人(店主)へ知らせる役はGASのメール、と分けるのがこの記事の原則です。

リマインド配信を作る

操作場所:エルメ管理画面 > 配信 > リマインド配信

  1. 「新規作成」を押し、配信名を「お弁当 前日リマインド」にする
  2. リマインドの基準(終了日)に、章3のフォーム「お弁当予約」の質問「受取日」を選ぶ
  3. 配信タイミングを「1日前の 20:30」にする
  4. 配信条件に「タグ 予約_キャンセル が付いていない」を設定する(キャンセルした人には送らない)
  5. 本文に下のテンプレを貼って保存し、有効にする
当日受取にリマインドを送らない理由:当日分はフォームの回答後メッセージ(章3)が、販売の数時間前に届く確認そのものです。もう1通送ると通数(章6)を消費するだけで、効果はほとんど変わりません。
締切後に入った翌日分はどうなるか:前日20:30のリマインドが出たあと、たとえば21:00に翌日分の予約が入ると、その人にリマインドは届きません。ただし回答後メッセージは届いているので、予約が消えるわけではありません。GASが「締切後の予約」として店主にメールするので、必要なら1:1チャットでひと言添えられます。

コピペ用テンプレ(前日リマインド)

明日のお弁当のご予約、お預かりしています。 受取日 :{受取日} 受取場所:{受取場所} お弁当 :{お弁当} × {個数} 販売時間は11:30〜14:00です。 お越しになれなくなった場合は、 下のメニューの「予約の確認」から キャンセルをお願いします。
明日のお弁当のご予約、お預かりしています。受取日:2026/10/11/受取場所:中央公園前/お弁当:から揚げ弁当 × 1。販売時間は11:30〜14:00です。
◆ ◆ ◆

6. 販売スケジュール通知とリッチメニュー

予約をしていないお客様にも「今日はここに出ています」を届けます。配信は毎朝9時。リッチメニューはトーク画面の下に常に出ているボタンなので、思い立ったときすぐ予約に入れます。

一斉配信を予約する

操作場所:エルメ管理画面 > 配信 > 一斉配信

  1. 「新規作成」を押し、配信名を「本日の販売案内」にする
  2. 配信対象を「全員」にする(絞るなら 友だち_新規 を含む条件で)
  3. 配信方法を「日時指定」にし、当日の 9:00 を設定する
  4. 本文に下のテンプレを貼り、場所とメニューを当日の内容に書き換える
  5. 本文の下にボタンを追加し、章3のフォームのURLを開く動作にする
配信は9:00、当日受取の締切は10:30です。案内を見てから予約するまでに90分の余裕があるよう設計しています。配信時刻を遅らせるときは、締切も一緒に見直してください。

無料枠200通の数え方

LINE公式アカウントの無料(コミュニケーション)プランは月200通までです。数えられるのはこちらから送るPushで、相手の操作に返すReplyは数えられません。この記事の仕組みを分けると次のとおりです。

この記事のメッセージ種類通数
あいさつメッセージ(章2)Reply数えない
リッチメニューのタップで返す案内(章6・7)Reply数えない
キーワード応答(章7)Reply数えない
フォーム回答後メッセージ(章3)Push1通
前日リマインド(章5)Push1通
本日の販売案内(章6)Push友だち数ぶん
一斉配信が一番効きます。友だち50人に毎日送ると1日50通、4日で上限です。友だちが20人を超えたらライトプラン(月5,000通)を前提に計画してください。無料枠で回すなら、一斉配信を「出店場所が変わる日だけ」に絞り、普段はリッチメニューの「今日の販売場所」(Reply)で見に来てもらう形が現実的です。

コピペ用テンプレ(本日の販売案内)

本日の販売のご案内です。 場所 :中央公園前 時間 :11:30〜14:00 雨天時:駅前ビル1階 軒下に変更 本日のお弁当 ・日替わり弁当(豚の生姜焼き)¥550 ・から揚げ弁当 ¥580 ・野菜たっぷり弁当 ¥520 売り切れ次第終了です。 取り置きのご予約は本日10:30まで、 下の「予約する」からどうぞ。

リッチメニューを作る

操作場所:エルメ管理画面 > 友だち管理 > リッチメニュー

位置ラベル動作つながる先
左上予約する章3のフォームURLを開くお弁当予約フォーム
右上今日の販売場所テキスト「販売場所」を送信キーワード応答(当日の案内)
左下予約の確認テキスト「予約確認」を送信章7のキーワード応答
右下お問い合わせテキスト「問い合わせ」を送信キーワード応答(連絡先の案内)
  1. 「新規作成」を押し、テンプレートから2×2の4分割を選ぶ
  2. 1200×810ピクセルの画像を用意してアップロードする
  3. 4つの領域それぞれに、上の表のとおり動作を設定する
  4. 表示設定を「全員に表示」にして、公開する
  5. LINE公式アカウントの管理画面側で作ったリッチメニューがあれば、オフにする(エルメのものと二重になるため)
「今日の販売場所」の応答は毎朝1回、文面の差し替えが必要です。場所が固定の曜日ローテーションなら、曜日ごとの文面を1通にまとめて書いておけます。毎日変わる運用なら、朝の一斉配信の作業とまとめて更新してください。
◆ ◆ ◆

7. 予約の確認・キャンセル・受渡し済み管理

章6のリッチメニューに「予約の確認」を置いたので、その受け皿を作ります。あわせて、販売現場での受渡し記録と当日の準備数の数え方をまとめます。

お客様側:予約の確認とキャンセル

操作場所:エルメ管理画面 > 自動応答 > キーワード応答

  1. キーワード「予約確認」で発動する応答を新規作成する
  2. 返信本文に、章3のフォーム回答(受取日・受取場所・お弁当・個数)を差し込む。本文の下に「この予約をキャンセルする」ボタンを置き、テキスト「キャンセル」を送信する動作にする
  3. キーワード「キャンセル」で発動する応答を作り、「キャンセルを承りました」の一言と、タグ付与 予約_キャンセル を設定する
  4. 店主がキャンセルにすぐ気づけるよう、顧客対応 > 通知設定 でチャットの通知をスマホアプリに出す設定にしておく
差し込みに入るのは直近の回答だけです。同じ人が2回予約すると、確認で表示されるのは後の予約です。1人が複数日ぶんをまとめて予約する運用なら、確認はスプレッドシートを見た店主が1:1チャットで答える形にしてください。
キャンセルは自動で台帳に反映されません。店主が通知を見て台帳のJ列を「キャンセル」に手で書き換えます。台帳側を自動更新するには注文IDをお客様に紐づける追加設計が必要で、1人〜数人で回す規模なら手作業のほうが確実です。タグ 予約_キャンセル が付いた時点で章5のリマインドは止まるので、お客様への連絡は自動で正しく止まります。

店主側:受渡しの記録

  1. 販売中はスマホで 注文台帳 を開いておく
  2. お客様が来たら、その行のJ列を「受付」から「受渡済」に変える
  3. 閉店後、J列が「受付」のまま残っている行が受け取りに来なかった予約になる

当日の準備数を数える

章4の morningReport が毎朝メールで届けますが、シート上でも見たいときは、台帳の右側の空いているセル(例:M1)に次の式を貼っておくと、当日ぶんの必要数がメニュー別に自動で出ます。

=QUERY(注文台帳!D2:J,
  "select F, sum(H)
   where D = date '"&TEXT(TODAY(),"yyyy-mm-dd")&"'
     and J = '受付'
   group by F
   label sum(H) '必要数'", 0)
D列(受取日)は章4のGASが日付型で書き込むので、この式はそのまま動きます。手入力で行を足すときだけ、D列に文字ではなく日付を入れるよう注意してください。

数字の振り返りに使う式

見たいこと式の例
今日の予約売上=SUMIFS(注文台帳!I:I, 注文台帳!D:D, TODAY(), 注文台帳!J:J, "受渡済")
メニュー別の注文数=COUNTIF(注文台帳!F:F, "から揚げ弁当")
受け取りに来なかった数=COUNTIFS(注文台帳!J:J, "受付", 注文台帳!D:D, "<"&TODAY())
場所別の予約数=COUNTIF(注文台帳!E:E, "中央公園前")
◆ ◆ ◆

まとめ

友だち追加から予約受付、台帳への自動取込、前日リマインド、受渡しの記録まで、路上弁当販売の一連の流れをLINEとスプレッドシートの2つだけで組みました。設計の勘所は3つです。

  • LINEのWebhookはエルメが占有する。お客様個人に送るのはエルメ(回答後メッセージ・リマインド配信)、GASは連携シートから台帳への取込と店主へのメールに徹する
  • スプレッドシートの列と、フォームの質問名と、タグ名は最初に1つずつ確定させ、以降ずらさない。メニューの選択肢はマスタと一字一句揃える
  • 受付の締切とリマインドの時刻はセットで決める。片方だけ動かすと届かない予約が生まれる

最初に手をつけるなら、章1のスプレッドシート2枚を作るところからです。ここさえ固まっていれば、あとの章は上から順に積み上げるだけで動きます。

運用上の補足:路上での弁当販売そのものには、食品衛生法にもとづく営業許可や、販売場所に応じた道路使用許可・場所の使用許諾などが必要になる場合があります。要件は自治体によって異なるため、開業前に所轄の保健所と警察署にご確認ください。この記事が扱うのは、許可を得たうえでの受注業務の設計部分です。
※ 本記事は架空の路上弁当販売店を想定したサンプルの構築例です。掲載コードはサンプルであり、ご利用は自己責任でお願いします。