GoogleFormで定員管理付き予約フォームを作る

2023-11-08(We)

モチベーション

チケットの票券管理にはいろんな方法があると思うのですが、当日精算(決済機能不要)のみの公演の場合、予約フォームって単にリストアップの機能だけがあればいいのかなと思って、その場合無料だったり安価な票券管理プラットフォームを使うと思うのですが、結構サービスが落ちていたりとかして、たまに話題になってますね。

(あれよく知らないのですが、人気のカンパニーが使うことで過剰に負荷かけちゃってサーバーを落としちゃってるってことなんでしょうか?🤔)

公演が近くなって落ちちゃったりしたら機会損失だし、それが他のカンパニーのせい?とかになるとやるせないですよね。

そこで、誰でも(Googleアカウントがあれば)使えるツールだけを使って、簡単に予約フォームを作ってみたので、方法をシェアしようと思います。

断っておきますが、もし本当に使う場合は自己責任でお願いします🙏

あと、最初に書くことじゃないですが、既存の枯れた信頼のあるサービスを使えるならそちらを使うほうがもちろんいいです。

一応、できるよ、というご紹介です。

Googleフォームでできそう

Googleフォーム自体は誰でも作成できて、内容も簡単にカスタマイズできますし、よく知られているツールなので利用側にとっても最低限の信用もありますよね。

ただ普通に作っただけでは演劇の予約フォームとしては大きな問題があって、定員管理ができないです。

Googleフォームで定員管理(50枚以上は予約できない)をしようと思ったら、毎日あるいは予約が入るごとにチェックして、定員に達していたらフォームの選択肢をいじってその回を削除、とかやらないといけなくてしんどいです。

しかしGASというのを組み合わせると、これもなんとか達成することが出来ます。

GASで定員管理

GASは Google App Script の略で、スプレッドシートやフォームなどをプログラマブルに扱うための機能です。エクセルマクロみたいなものですね。

エクセルマクロとは違って JavaScript で操作することができます。

コピペでも問題ないですが、GASで簡単な操作を書くために必要なくらいは、数週間の勉強でできるようになると思います。

これを使って、以下のようなことをして定員予約を実現します。

  • Googleフォームから回答が送信されたら、スプレッドシートに追記(これは普通にフォームの機能)
  • GASのイベントトリガーを使って、スプレッドシートにデータが追加されたときに関数を実行
  • GASに定義した関数で、定員に達した回があるか集計し、もしあればGoogleフォームの選択肢から削除
  • 同様に、前日の24時を回ったら選択肢から削除

こうすることで、例えば50人定員の場合、50枚目を誰かが予約したらGoogleフォームからその回の選択肢が消え、次にアクセスしたときには表示されなくなっています。

作成手順

順を追って作成手順を紹介します。

Googleフォームを作る

まずは新しいGoogleフォームを作りましょう。

そして質問項目を作っていきます。このへんは要件によると思いますが、質問の内容によってあとで作るGASスクリプトの内容が変わります。

とりあえず、メールアドレス、お名前、予約日時の項目を追加しました。今回はこれだけあればいいという要件でやっていきます。

予約日時の選択肢はあとでセットするので空で大丈夫です。

スクリーンショット: Googleフォームの質問作成画面

ちなみに、必須スイッチの右にある3点メニューから「回答を検証」を選択すると、既定のバリデーションを行うことが出来ます。 「メールアドレス」の項目では、「回答を検証」を選んで追加される入力欄で「テキスト > メールアドレス」と選んでおくと、メールアドレスとして正しくない文字列を入力時に弾いてくれるのでやっておくといいと思います。

紐づいたスプレッドシート作成

続いて、フォームの編集画面の回答タブから、「スプレッドシートにリンク」をクリックして、「新しいスプレッドシートを作成」を選択し作成します。

スクリーンショット: からのスプレッドシート

すると、このようにスプレッドシートがひらかれます。

ここに回答がたまっていく感じですね。

この回答一覧のシート名は「予約一覧」にしておきます。

予約日時の選択肢一覧を定義

予約日時の選択肢一覧をシートで定義しておきます。

新しく「予約日時選択肢一覧」というシートを作成して、A列に日付、B列に時間を記述しておきます。

A列全体を選択して「データの入力規則」から「有効な日付」を選択して、それにマッチするように書くと後々の処理でエラーになりません。

B列も全体を選択して、表示形式から「数値 > 書式なしテキスト」にしておきましょう。

スクリーンショット: 予約日時選択肢一覧シート。A列に「2023/11/08」など日付が、B列に「15:00」などの時間が書かれている。

B列はこのように左詰めになってると書式なしテキストにできていると思います。右詰めになってると、内部で時間データとして扱われてるぽいです。

GoogleFormからAppScriptを作成

フォームの編集画面に戻り、右上の「送信」ボタンの横にあるメニューから「スクリプトエディタ」を開きます。

スクリーンショット: スクリプトエディタを選択

そしたらこんなのが開きます。

スクリーンショット: AppScriptを開いたところ

ここに色々書いていきます。JavaScript見たことがないとちょっとウッとなるかもですが、最初からあるものは全て消してとりあえず以下をコピペしてください。

ちょっとこのブログでコード例記述すること無いと思ってたので、ハイライトも何もないんでみづらいですが…

AppScriptのほうに貼り付ければ色分けされて見やすくなります。

const SPREADSHEET_ID = 'YOUR_SPEREADSHEET_ID' // 自分の環境に合わせて書き換えてください
const RESERVED_LIST_SHEET_NAME = '予約一覧' // 自分の環境に合わせて書き換えてください
const SELECTION_LIST_SHEET_NAME = '予約日時選択肢一覧' // 自分の環境に合わせて書き換えてください
const LIMIT = 50 // 定員を指定してください
const RESERVATION_DATETIME_FIELD_NAME = '予約日時' // フォームで、予約日時に該当する質問の項目名を記述してください
const SPREADSHEET = SpreadsheetApp.openById(SPREADSHEET_ID)
dayjs.dayjs.locale.ja
dayjs.dayjs.locale('ja')

/**
 * 選択肢のマスターデータ取得
 */
const getSelectionList = () => {
  const selectionListSheet = SPREADSHEET.getSheetByName(SELECTION_LIST_SHEET_NAME)
  const selectionList = selectionListSheet.getRange(1, 1, selectionListSheet.getLastRow(), 2).getValues().map(([date, time]) => {
    return `${dayjs.dayjs(date).format('YYYY/MM/DD')} ${time}`
  })
  return selectionList
}
/**
 * 日時ごとの予約状況を集計
 */
const getReservationSummary = () => {
  const sheet = SPREADSHEET.getSheetByName(RESERVED_LIST_SHEET_NAME)
  const reservationLength = sheet.getLastRow() - 1
  if (reservationLength) {
    const reservations = sheet.getRange(2, 2, reservationLength, 3).getValues().reduce((acc, cur) => {
      if (cur[2] in acc) {
        return {
          ...acc,
          [cur[2]]: [...acc[cur[2]], cur[1]] 
        }
      } else {
        return {
          ...acc,
          [cur[2]]: [cur[1]]
        }
      }
    }, {})
    return reservations
  } else {
    return []
  }
}
/**
 * 定員に達した日時を除いた選択肢の一覧の取得
 */
const getRemainingSelectionList = () => {
  // マスターデータ
  const selectionList = getSelectionList()
  const sheet = SPREADSHEET.getSheetByName(RESERVED_LIST_SHEET_NAME)

  // 予約一件もない場合はマスターデータのうち過去以外のもの返す
  const reservationLength = sheet.getLastRow() - 1
  if (!reservationLength) return selectionList.filter(date => !isPast(date))

  // 予約ある場合は定員に達していない回だけ返す
  const reservations = getReservationSummary()
  const remainingSelectionList = selectionList.filter(selection => (!(selection in reservations) || reservations[selection].length < LIMIT) && !isPast(selection))
  return remainingSelectionList
}
/**
 * 選択肢初期化
 */
function initFormSelectionList () {
  const form = FormApp.getActiveForm()
  const items = form.getItems()
  const selectionList = getSelectionList()
  items.forEach(item => {
    if (item.getTitle() === RESERVATION_DATETIME_FIELD_NAME) {
      const question = item.asListItem()
      question.setChoices(selectionList.map(d => question.createChoice(d)))
    }
    return
  })
}
/**
 * フォームの選択肢更新
 */
function updateForm() {
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID)
  const sheet = ss.getSheetByName(RESERVED_LIST_SHEET_NAME)

  Utilities.sleep(1000)

  // 予約無い場合は何もしない
  const reservationLength = sheet.getLastRow() - 1
  if (!reservationLength) return

  // 残りの候補日取得
  const remainingSelectionList = getRemainingSelectionList()

  const form = FormApp.getActiveForm()
  // 質問項目
  const items = form.getItems()
  items.forEach(item => {
    if (item.getTitle() === RESERVATION_DATETIME_FIELD_NAME) {
      const question = item.asListItem()

      // 選択肢がまだある場合はセット
      if (remainingSelectionList.length) {
        form.setAcceptingResponses(true)
        question.setChoices(remainingSelectionList.map(date => question.createChoice(date)))
      }
      // ない場合は受付停止にする
      else {
        form.setAcceptingResponses(false)
      }
    }
    return
  })
}
/**
 * 過去かどうか判定
 */
function isPast(selection) {
  const today = dayjs.dayjs().startOf('date').format('YYYY-MM-DD')
  const target = dayjs.dayjs(selection).startOf('date').format('YYYY-MM-DD')
  return today >= target
}

必要なライブラリの追加

日付操作に便利な dayjs というライブラリを追加します。

スクリプトエディタの左メニュー内にある「ライブラリ」の横にあるプラスボタンを押します。

スクリプトIDを検索してライブラリを追加できます。dayjsのスクリプトIDは「1ShsRhHc8tgPy5wGOzUvgEhOedJUQD53m-gd8lG2MOgs-dXC_aCZn9lFB」みたいなのでこれを入力して検索します。

見つかったら追加を選択します。追加できたら左メニューにこんな感じで表示されてるはずです。

スクリーンショット: 左メニューのライブラリのところにdayjsが追加されている

スプレッドシートIDを記述

手元で作成したスプシに合わせて、各値を少しいじります。

まずスプレッドシートのIDを調べます。

スプレッドシートのIDは、先程作ったスプレッドシートを開いてURLを見て調べます。「https://docs.google.com/spreadsheets/d/1V5yHBFnD4a5C1DaAzE41LI-izEND0D0wNDpx2S9_tLw/edit?resourcekey#gid=358056622」みたいなURLになっていると思いますが、これの spreadsheets/d/ 以下から、 /edit?... の直前まで、つまりこれでいうと 「1V5yHBFnD4a5C1DaAzE41LI-izEND0D0wNDpx2S9_tLw」になります。

それをコピペしてスクリプトエディタ1行目の 「YOUR_SPREADSHEET_ID」の部分に貼り付けてください。

その他必要なシート名を変更

フォームの回答がたまってくシートのシート名を2行目の「予約一覧」を、

予約日時の選択肢を定義したシートのシート名を3行めの「予約日時選択肢一覧」の部分にそれぞれペーストしてください。

定員は4行めの「50」のところを書き換えてください。

フォームで、予約日時を選択する質問の項目名を5行目の「予約日時」のところに書き換えてください。

例👇

const SPREADSHEET_ID = '1V5yHBFnD4a5C1DaAzE41LI-izEND0D0wNDpx2S9_tLw' // 自分の環境に合わせて書き換えてください
const RESERVED_LIST_SHEET_NAME = '予約一覧' // 自分の環境に合わせて書き換えてください
const SELECTION_LIST_SHEET_NAME = '予約日時選択肢一覧' // 自分の環境に合わせて書き換えてください
const LIMIT = 50 // 定員を指定してください
const RESERVATION_DATETIME_FIELD_NAME = '予約日時' // フォームで、予約日時に該当する質問の項目名を記述してください

スクリプトから選択肢を追加してみる

エディタ上部のプルダウンから関数「initFormSelectionList」を選択して、実行を押します。

スクリーンショット: 実行する関数に initFormSelectionList を選択

初めて実行する場合は権限の許可を求められると思います。

ポップアップでGoogleアカウントを選択すると、こういう画面が開きます。

スクリーンショット: 実行する関数に initFormSelectionList を選択

なんか怖い画面ですが、要はGoogle製じゃない(ユーザーの手作り)ということの確認なので大丈夫です。

小さい「詳細」というボタンをクリックして、最下部の「「プロジェクト名」(安全でないページ)に移動」をクリックします。

次の画面で「許可」を選択してください。(ユーザー手作りのプログラムがスプシやフォームを操作しますよという権限です)

うまくいくと、フォームの選択肢が初期化され、先程まで空だったのが「予約日時選択肢一覧」で定義した選択肢になっているはずです。

スクリーンショット: フォームの編集画面で選択肢が追加されている

トリガーを指定

AppScriptは特定のタイミングで任意の関数を実行できるトリガーという機能があります。

左メニューの目覚まし時計みたいなマークの「トリガー」を選択してトリガーを追加してください。

そしたらこんな感じで

  • 「実行する関数を選択」に「updateForm」を
  • 「イベントのソースを選択」に「フォーム」を
  • 「イベントの種類を選択」に「フォーム送信時」

を追加します。

スクリーンショット: トリガー追加画面

こうすると、フォームでデータを送信するたびに、「updateForm」関数を実行してくれます。

「updateForm」関数は予約リストを確認して、定員に達している回と、前日の24時を回っている回を除いて選択肢をセットしなおしています。

もう一つ、これを毎日実行して公演日の前日24時を回っていたら選択肢から削除できるようにトリガーを登録します。

  • 「実行する関数を選択」に「updateForm」を
  • 「イベントのソースを選択」で「時間主導型」を
  • 「時間ベースのトリガーのタイプを選択」で「日付ベースのタイマー」を
  • 「時刻を選択」で「0時〜1時」を

選択してトリガーを追加します。

これで毎日0時〜1時の間に選択肢をセットしなおしてくれます。

動作確認してみる

フォームのプレビュー画面から、予約してみましょう。

適当な日付を選択して、予約します。

定員がデフォルトの50の場合、プレビュー画面をリロードしても選択肢は変わっていないはずです。

次に、スクリプトエディタ4行目で定員を2にします(保存を忘れない)。

そして先程と同じ回を予約して、別の回答を送信でフォームを更新して見てみると、

スクリーンショット: 選択肢から11月10日の回が消えている

ちゃんと予約日時の選択肢から消えています!!

まとめ

ということで、Googleフォーム+GASで定員管理付き簡易予約フォームを作成する方法をご紹介しました。

予約がポツポツ入る感じで当日精算しかない、という場合はこれでもやれるとは思います。

一応以前お手伝いした小規模な展示の予約管理に使ったときはこれでなんとかなりました。

難点としては、スプレッドシートをDB代わりにしているということで、トランザクション処理ができないのが致命的ですね。

ほぼ同時に(ミリ秒単位?)予約すると、定員+1枚予約できてしまいます。

まあ、これを使うくらいの規模ならあまり気にならない確率かもしれませんが。

冒頭でも書いた通り、他のサービスを使えるならそちらを使ったほうがいいですがジャストアイディアとしてはありなんじゃないかなとは思います。

GASが書ければメール送信や、キャスト別予約なども作れると思います。