満席になった瞬間、受付ページが自分から閉まる|申込・予約 秘書【応用編その4】

定員も締切も、セルに書くだけ——満席の札がひとりでに掛かる講座会場の入り口の水彩イラスト つまずき帳

「受付システムが完成しました」とは、言いませんでした——あれは、予告でした

基礎編の最終回を、わたしはこう締めました。

「受付システムが完成しました」とは、言いません。この受付ページは、7つの引き出しを1本ずつ開けるための、ちょうどいい練習台でした。

あのときの受付ページは、申込を受け取って、確認メールを返して、控えPDFを作って、毎朝まとめて、LINEを鳴らして——「届いたものをさばく」ことは、ひととおりできるようになっていました。でも、受付という仕事のいちばん重いところには、まだ手を付けていませんでした。

受付で本当に神経を使うのは、受け取ることではなく、「締める」ことです。

定員のある講座や説明会をやったことのある人なら、覚えがあると思います。案内文には「定員になり次第、締め切ります」と書く。でも、実際に締めるのは誰かというと——人間です。申込の数を目で数えて、定員に達したらフォームを閉じに行く。閉じ忘れれば、定員を超えて受けてしまう。夜中に届いた11人目に「実は満席でして」と、いちばん送りたくないお詫びのメールを書く。締切日を過ぎたフォームが開きっぱなしで、翌朝あわてて閉じる。「定員になり次第」の裏側は、ずっと人間の見張り番でした。

あの最終回の締めの一文は、謙遜ではありませんでした。予告でした。 今日、その続きを作ります。受け取るだけの練習台を卒業して、「締める」ができる受付——申込・予約 秘書を、AIと一緒に雇います。

結果から言います = 満席になった瞬間、受付ページが自分から閉まる

先に、実機テストの結果をそのまま書きます。

定員を2にして、スマホから申し込んでみました。1件目——「申し込む」を押すと「送信中…」に変わり、「受け付けました」。確認メールと控えPDFが届きます。2件目——同じく「受け付けました」。これで定員ちょうどです。

そして3件目。同じURLを開いた瞬間、フォームは、もう出てきませんでした。 代わりに表示されたのは、これだけです。

満席となりました。
たくさんのお申し込みをありがとうございました。

わたしは、何もしていません。フォームを閉じに行っていないし、設定も触っていません。2件目が入った時点で定員に達したことを、ページが自分で数えて、自分で閉まったんです。

締切も同じです。『設定』シートの締切日時を過ぎると、同じURLが今度は「受付は終了しました。」の画面に変わります。わたしがやるのは、設定シートのセルに数字を書くことだけ。 定員は「2」、締切は日付。あとの見張り番は、ぜんぶ秘書の仕事です。

そして毎度のことですが、今回もわたしはコードを一行も書いていません。 書いたのはAIで、わたしがやったのは、できあがったものを貼り付けて、一度だけ許可して、設定シートを埋めて、公開ボタンを1回押しただけ。それが、このブログのやり方です。

今日の一点 = コードは今日の1回きり。貼り直しも、公開し直しも、もうしません

この「申込・予約 秘書」は、前作の受発注・請求 秘書と同じ方式で行きます。すなわち、完成形のコードを、最初に1本だけ貼り切る方式です。

今日貼るコードの中には、じつは今日使わない道具が、もうぜんぶ入っています。キャンセル待ちの列を作る仕組みも、申込と確定のあいだに人の判断を挟む承認制も、前日リマインダーも、スマホで開ける管理ページも。この先の「育てる編」で起きる変化は、『設定』シートのセルをONにするか、メニューを押すか、それだけです。

だから、先に約束しておきます。

コードの貼り直しも、公開ページのデプロイし直し(再デプロイ)も、もうしません。 申込者に案内するURLは、今日作ったものが、最後までそのまま使えます。

基礎編を歩いてくれた人ほど、この約束の重みが分かると思います。あのシリーズでは「本流に手を入れたら公開し直し」を何度も学びました。今回はその心配ごとを、最初の1回に全部押し込んであります。

なお、今回作るのは新しいスプレッドシートの上です。基礎編で作った受付ページがもう動いている人も、あちらには一切触りません。台紙を分けて、まっさらから雇い直します。

用意するもの = たった4ステップ

この秘書を動かすまでにやることは、4つだけです。どれも、コードを読む必要はありません。

はじめる前の準備がひとつだけ。まっさらな新しいGoogleスプレッドシートを1枚、作っておいてください(Googleドライブで[新規]→[Googleスプレッドシート])。名前は「申込・予約秘書」など、あとで分かるものに。基礎編の受付ページとは別の、新しい台紙です。

① AIにプロンプトを貼って、コードをもらう

この秘書の中身(コード)は、自分では書きません。ChatGPTやGeminiのような対話AIに、決まった「お願い文(プロンプト)」を丸ごとコピペして頼むだけです。プロンプトの全文は、この記事の後半「AIに渡すプロンプト(全文)」に載せてあります。中身をあなたが読んで理解する必要は、まったくありません。

返ってきたコードは、さきほど作ったスプレッドシートの[拡張機能]→[Apps Script]に貼り付けて、保存します。開き方と、許可(アクセスの承認)画面の進み方は、許可画面の回に写真つきでまとめてあります。なお、保存しただけでは許可画面は出てきません。出るのは、次の②で「初期化」を押した瞬間です。

ひとつだけ、許可画面の予告を。今回の許可一覧には、メール送信のほかにGmailの下書きに関する許可が並びます(環境によっては「メールの作成・送信・削除」のような強い文言で表示されることがありますが、驚かなくて大丈夫です)。これは、この秘書が「お断りのご連絡」のような重いメールを、自動送信ではなく下書きまでしか作らない設計だからです(育てる編②で登場します)。実際に作るのは下書きだけ。勝手に送ることはありません。ほかにドライブや外部サービスへの接続の許可も並びますが、これは控えPDFの保存やAI・LINE連携のためで、基礎編でおなじみの顔ぶれです。

② 最初に一度、「初期化(シートを作る)」を押す

コードを貼って保存し、スプレッドシートを再読み込みすると、上のメニューに「申込・予約秘書」が増えています。これを開いて、「初期化(シートを作る)」を押してください。初回だけ、ここで許可(アクセスの承認)の画面が出ます。承認したら、もう一度「初期化」を押してください。次の2枚のシートが自動でできます。

  • 設定:イベントの情報と、秘書のスイッチ類を書く場所。触るのはここだけ
  • 申込一覧:申込1件=1行の台帳。受付日時・お名前・メール・参加人数・ひとことなどの列に加えて、「状態」(確定/承認待ち/キャンセル待ち/キャンセル/お断り)の札が付きます。AIの仕分けメモ用と、秘書が送った通知の記録用の列もありますが、今日は空のままで正常です

この初期化も、受発注のときと同じ「無ければ作る」だけです。すでにあるシートを消したり上書きしたりはしません。何度押しても、増えもしません。

③ 『設定』シートのB列を埋める

『設定』シートは、A列に項目名が縦に並んでいて、あなたが書くのはB列だけです。

『設定』シート。イベント名・開催日・開始時刻・場所・定員・締切日時・承認制・キャンセル待ち・残席表示・主催者名をB列に書くだけ
項目 何を入れるか
イベント名受付ページとメールに出る催しの名前
開催日・開始時刻・場所案内文に差し込まれます
定員例:10(数字だけ)
締切日時例:2026/07/12 18:00
承認制(ON・OFF)今日は OFF のまま(育てる編②)
キャンセル待ち(ON・OFF)今日は OFF のまま(育てる編①)
残席をフォームに表示(ON・OFF)既定 ON。表示は“目安”です(後述)
主催者名メールの署名に入ります

ここで、わたしが実際に踏んだ注意をひとつ。開催日や締切日時のセルに、「明日」「明後日」のような言葉を書いてはいけません。 わたしは最初、テストのつもりで「明後日」と書いて、案内文に開催日時が出てこない・締切がいつまでも来ない、という不思議な状態になりました。秘書は読めない日付を「無いもの」として安全側に倒すので、壊れはしません。でも、意図とは違う動きになります。日付は 2026/07/13 のような、本物の日付形式で。 セルを選んだとき数式バーに日付として表示されていれば大丈夫です。

④ 公開(デプロイ)を1回だけ

最後に、このページを世界に開きます。Apps Scriptの画面から「デプロイ」→「新しいデプロイ」で、種類は「ウェブアプリ」。設定は基礎編の受付ページの回と同じ、「実行するユーザー=自分」「アクセスできるユーザー=全員」です。ここでの「全員」は、「URLを知っている人なら誰でも、この申込フォームを開ける」という意味です。公開の受付ページなので、これで正しい設定です。スプレッドシートの中身や、ほかの申込者の情報が誰かに見えるようになるわけではありません。だからこそ、発行された /exec で終わるURLは、案内したい相手にだけ渡してください。それが、申込者に案内する受付ページになります。

——これで、準備はおしまいです。デプロイのボタンを押すのは、今日のこの1回だけ。この先の育てる編で、もう一度押すことはありません。

AIに渡すプロンプト(全文・中身は読まなくて大丈夫)

下の枠をまるごとコピーして、対話AIに貼って送るだけです。枠の中は長くて、見慣れない専門用語も並びますが、あれは全部AIへの「お願いごと」。長いのは、「やってほしいこと」と「やってほしくないこと」を正直に念押ししているからです。途中で切らずに、枠ごと一度にコピーしてください。

“`text

あなたはGoogle Apps Script(GAS)の熟練エンジニアです。これから指示する仕様を満たす「自分専用の申込・予約 秘書」のGASを、コピーして貼ればそのまま動く完成形で書いてください。専門用語の説明は不要です。コードと、コード内の日本語コメント(非エンジニアにも分かる代弁口調・誇大表現なし・正直)だけを返してください。冒頭に長い前置きはいりません。コメントは実際のコードの動きと必ず一致させ、「やる」と書いて実装しない死にコメントは残さないでください。仕様が多いので、下の【章】を上から順に、ひとつのスクリプトとして全部実装してください。

【作るもの(一言)】

スプレッドシートに紐づくGASのWebアプリ。講座・イベントの申込受付を「受け取る」だけでなく「締める」まで自動化する。公開ページ(doGet)が開かれるたびに『設定』シートを読み、締切超過なら受付終了画面・満席なら満席画面・空きがあれば申込フォームを、ページ自身が判断して出し分ける(=満席になった瞬間、受付ページが自分から閉まる)。申込は五段構え(記録→確認メール+控えPDF→Drive保存→AI仕分け→LINE通知)で処理し、キャンセル待ちの繰り上げ・承認制・前日リマインダー・毎朝まとめ・スマホ用管理ページまで、この1本に全部入れる。最初に1回貼ったら、以降コードの貼り直しも公開ページの再デプロイもしない(変化はぜんぶ設定セル・メニュー・トリガーのデータ駆動)。

【絶対にやらないこと(正直に。コメントにも明記)】

  • お断りメールは自動送信しない。GmailApp.createDraft で Gmail の「下書き」を作るところまで(文面を人が整え、送信ボタンは人が押す=誤送信防止)。
  • 申込者に向けて送る自動メール(確認・承認確定・繰り上げ)と、状態を書き換える本体処理は、関数名の末尾に「_」を付けてGASの非公開関数にする(末尾「_」の関数は google.script.run から呼べない)。「_」を付けるのはこの5つだけ:状態別確認メールを送る_/承認確定メールを送る_/繰り上げメールを送る_/承認を実行_/キャンセルと繰り上げを実行_。宛先や本文を引数で受けるメール関数が公開ページの裏側からそのまま叩ける状態だと、匿名の訪問者に「あなたのGmailから任意のメールを送らせる」「予約を勝手にキャンセルさせる」裏口になるため、名前ひとつでその裏口を塞ぐ。正規の入り口は、シートのメニュー(オーナーの操作)と、本人確認つきの管理ページ関数だけ。なお前日リマインダーは別枠:トリガーが文字列名で呼ぶハンドラ 前日リマインダーを送る()(末尾「_」なし・公開のまま。トリガーハンドラは google.script.run に露出しないので裏口にならない)の本体に、メール送信をインラインで実装する(別建ての非公開メール関数は作らない)。
  • 破壊的操作はしない(読む・書き足す・新規シート/フォルダ/PDFを作るだけ)。初期化は「未存在のシートだけ」を作り、既にあるシート・データには1文字も触れない。何度実行しても増えない。
  • 送金・決済は一切扱わない。個人情報を増やさない(毎朝まとめとLINE通知にメールアドレス列は載せない)。
  • AIに数を数えさせない。残席・定員・締切の判定はすべてコードの決定論的計算。AI(Gemini)がやるのは「ひとこと」欄の仕分けだけ。

【秘密情報(直書き禁止)と任意機能】

GeminiのAPIキーとLINEのチャネルアクセストークンは PropertiesService.getScriptProperties() から読む(プロパティ名は GEMINI_API_KEY と LINE_TOKEN)。どちらも任意=未設定なら該当機能だけを“静かにスキップ”し、受付(記録・確認メール・控えPDF・Drive保存)はメールだけで完全に動くこと。

【時刻と実装形式の約束】

  • 日付・時刻はぜんぶ東京時間で扱う。定数 var TIMEZONE = 'Asia/Tokyo'; を置き、締切判定・前日判定・控えの受付日時まで統一する(前提:appsscript.jsonと台紙スプレッドシートのタイムゾーンも「東京」。コメントで注意書き)。
  • フォームHTMLをテンプレートリテラル(バッククォート)で書く場合、その中の<script>内のJS文字列に改行を入れるときは必ず \\n と書くこと(\n と書くとテンプレートリテラルが実際の改行に変換してJS文字列が途中で切れ、送信ボタンが無反応になる実バグがある)。
  • 関数名・シート名・メニュー項目・メール件名・画面文言は、下の指定表記をそのまま使う。

────────────────────────────────────────

【章1|基盤(定数・『設定』・『申込一覧』・初期化・onOpenメニュー)】

────────────────────────────────────────

■定数:var SHEET_NAME = '申込一覧'; / var CONFIG_SHEET_NAME = '設定'; / var DRIVE_FOLDER_NAME = '申込・予約秘書_控え';(控えフォルダは秘書専用の別名=他ツールの『申込控え』とfind-or-createで衝突させない)/ var TIMEZONE = 'Asia/Tokyo'; / var 状態一覧 = ['確定', '承認待ち', 'キャンセル待ち', 'キャンセル', 'お断り'];(G列「状態」に入る5つの札。プルダウンもこの5値で固定)。

■onOpen:メニュー「申込・予約秘書」を作る。項目と呼ぶ関数(表記そのまま・区切り線つき):

『初期化(シートを作る)』→初期化 /(区切り)/『選択行を承認する』→選択行を承認する /『選択行をお断り下書きにする』→選択行をお断り下書きにする /『選択行をキャンセルにする(繰り上げつき)』→選択行をキャンセルにする /(区切り)/『前日リマインダーを予約する』→前日リマインダーを予約する /『前日リマインダーを解約する』→前日リマインダーを解約する /『毎朝まとめを予約する』→毎朝まとめを予約する /『毎朝まとめを解約する』→毎朝まとめを解約する /(区切り)/『管理ページのURLを表示』→管理ページのURLを表示 /『シートを整える』→シートを整える。

■初期化():『設定』『申込一覧』の2枚を“無いときだけ”作る(何度実行しても増えない・既存データは触らない)。

・『設定』=縦持ち(A列に項目名・B列に値。触るのはB列だけ)。10行を上から順に、初期値つきで:イベント名='テスト講座'/開催日=''/開始時刻=''/場所=''/定員=10/締切日時=''/承認制(ON・OFF)='OFF'/キャンセル待ち(ON・OFF)='OFF'/残席をフォームに表示(ON・OFF)='ON'/主催者名=''。A列は太字、列幅はA=230/B=280。

・『申込一覧』=8列の見出し:受付日時/お名前/メール/参加人数/ひとこと/AI仕分け/状態/通知記録。見出し太字+行固定。行順=申込順が、そのままキャンセル待ちの並び順になる。関数 状態プルダウンを確保(sh) がG列2行目以降(getMaxRows()-1行)に requireValueInList(状態一覧, true)+setAllowInvalid(true) の入力規則を仕込む(表記ゆれを入り口で防ぐ。手で違う文字を書いても“警告どまり”で壊さないが、通知は飛ばない)。

・控えフォルダもここで try { 控えフォルダ(); } catch (e) {} で先に1つ作っておく(saveEntryの初回に任せると、ほぼ同時の初申込2件が「棚が無い」と同時判定して棚が2つできる余地があるため)。

・最後に ui.alert:作ったら「〜を作りました。\nまず『設定』のB列(イベント名・定員・締切日時)を埋めてください。」/両方既存なら「2枚ともすでにあります(作り直していません)。このまま使えます。」

■小さなヘルパー群:

・申込一覧シート():『申込一覧』を取得。無ければ見出しつきで作る(find-or-create)。

・設定を読む():『設定』のB1〜B10を毎回読む。『設定』が無ければ「『設定』シートがありません。メニュー「初期化(シートを作る)」を先に実行してください。」を throw。返すオブジェクト:イベント名(trim・空なら'講座・イベント')/開催日/開始時刻/場所(trim)/定員(parseInt・NaNや負は0)/締切日時/承認制・キャンセル待ち(ON判定・空欄既定OFF)/残席表示(ON判定・残席表示だけ“空欄ならON”が既定)/主催者名(trim)。

・ON判定(v, 空欄のとき):trim+大文字化して 'ON' と一致なら true。空欄は引数の既定値。

・日時として読む(v):Dateならそのまま、文字列は new Date で解釈、読めなければ null(=設定なし扱い)。

・人数を矯正(v):parseInt して数字でない・1未満は「1」と数える(安全側=定員を守る方向に倒す)。

・残席を数える(sh, 設定):カウンタは持たず、毎回『申込一覧』を数え直す(=ドリフトしない)。席を使うのは状態が「確定」と「承認待ち」の行だけ(承認待ち=仮押さえ。定員超過を確実に防ぐ)。「キャンセル待ち・キャンセル・お断り」は席を使わない。残=定員−使用(負なら0)。

・締切を過ぎている(設定):締切日時が未設定なら“締切なし”=false。判定は new Date().getTime() > 締切(半開区間=ちょうどの瞬間はまだ受け付ける)。

・イベント案内文(設定):開催日があれば「開催日時:yyyy年M月d日(曜)+開始時刻〜」(開始時刻はDateならHH:mm、文字列ならそのまま。曜日は '日月火水木金土'.charAt)、場所があれば「場所  :…」。未記入の項目は行ごと省き、\n で結合して返す(メールにもフォームにも使い回す)。

────────────────────────────────────────

【章2|公開ページ(doGet三分岐+admin分岐・案内ページ・フォームは章3の後)】

────────────────────────────────────────

■doGet(e):公開ページの入り口。“開いた瞬間に”『設定』を読んで判断する。だからセルを書き換えるだけで公開ページの挙動が変わる(再デプロイなし)=この秘書の背骨。分岐は4つ:

(a) e.parameter.page === 'admin' → 管理ページ(章7)。管理者である() が false なら 案内ページHtml('主催者専用', 'このページは主催者専用です。') をタイトル『管理ページ』で返す。true なら 管理ページHtml() をタイトル『申込・予約秘書 管理ページ』で返す。

次に 設定を読む() を try で呼び、失敗(初期化前にURLを開かれた)なら GASの生エラー画面ではなく 案内ページHtml('準備中', 'このページはただいま準備中です。\nしばらくしてから、もう一度お越しください。') を返す保険を入れる。

(b) 締切を過ぎている(設定) → 案内ページHtml(設定.イベント名, '受付は終了しました。\nたくさんのお申し込みをありがとうございました。')。

(c) 満席(残席を数える の結果が0以下)かつ キャンセル待ちOFF → 案内ページHtml(設定.イベント名, '満席となりました。\nたくさんのお申し込みをありがとうございました。')。

(d) それ以外 → 申込フォームHtml(設定, 残席, 満席)(満席×待ちONは“キャンセル待ちとして受け付けます”の注記つきフォーム)。

どの分岐も .setTitle((b)(c)(d)は 設定.イベント名+' 申込受付')と .addMetaTag('viewport', 'width=device-width, initial-scale=1') を付ける(スマホ前提)。

■管理者である():Session.getActiveUser().getEmail() と、このスプレッドシートの getOwner() のメールが一致するときだけ true。未ログイン等で取れないときは空文字=false(見せない側に倒す)。両方 try/catch で握る。

■案内ページHtml(題, 本文):受付終了・満席・主催者専用・準備中に使う“1枚案内ページ”。フォームと同じ見た目のカード(max-width:480px・白背景・角丸14px・中央寄せ)に題と本文だけ。本文は white-space:pre-line で改行を生かす。差し込む値は esc()(&<>"' の5文字をHTMLエスケープ)を通す。

────────────────────────────────────────

【章3|saveEntry(LockService直列化・入口規約・五段構え)と申込フォーム】

────────────────────────────────────────

■saveEntry(data):この秘書の技術的な心臓部。窓口は一度にひとり(LockService)=同時に2人が申し込んでも定員を超えない。

・LockService.getScriptLock() で waitLock(10000)(最大10秒待ち)。取れなければ { ok: false, message: 'ただいま混み合っています。少し待ってもう一度お送りください' } を返す(申込は失われていない=もう一度送ればよい、と正直に)。

・ロックの中でやるのは【設定読取→締切再チェック→残席カウント→状態決定→appendRow】だけ。

 - 締切の再チェック:フォームを開いたまま締切を跨いで送信されるケースを、記録の直前でもう一度締める。超過なら { ok: false, message: '申し訳ありません。受付は終了しました。' }。

 - 入口規約:空きあり=「残席 >= 申込人数」。0<残席<人数(残り1席に2人など)は満席と同じ扱い。

  残席>=人数 → 状態は 設定.承認制 ? '承認待ち' : '確定'(承認制ON=人の判断を挟む。席は仮押さえ)。

  満たさず キャンセル待ちON → 状態 'キャンセル待ち'(満席・残席不足でも列に並んでもらえる)。

  満たさず 待ちOFF → { ok: false, message: 残席>0 ? '申し訳ありません。残席が申込人数に足りません(残り'+残席+'席)。' : '申し訳ありません。満席のため受付を終了しました。' }。

 - (1)記録:sh.appendRow([new Date(), data.name, data.email, 人数, data.note, '', 状態, '']) して行番号=getLastRow()。ここまでがロックの中=定員の判定と記録は誰にも割り込まれない。

・lock.releaseLock() は try/finally(何があっても窓口は必ず返す)。メール・PDF・AI・LINE(5〜15秒かかる仕事)は、必ずロックを返してから=次の人を待たせない。

・ロックの外=五段構えの(2)〜(5)を各段独立try/catchで:(2)(3) 状態別確認メールを送る_(data, 状態, 設定)(章4)/(4) AI仕分けして記録(sh, 行番号, data)(章8)/(5) 申込をLINEに知らせる(data, 状態, 設定)(章9)。後ろの段が失敗しても前の段は無傷。最後に { ok: true, 状態: 状態 } を返す。

■申込フォームHtml(設定, 残席, キャンセル待ち受付中):スマホ前提の1カードフォーム。

・頭のご案内:イベント名の見出し('○○ 申込受付')+イベント案内文(esc後、\n を <br> に)。

・残席の目安:キャンセル待ち受付中なら「現在満席です。キャンセル待ちとして受け付けます。<br>お席が空きましたらメールでお知らせします。」(琥珀色の注記)。そうでなく残席表示ONなら「残り○席(目安)」+小さく「※お申込人数分の空きがない場合は、満席またはキャンセル待ちのご案内になります。」。“目安”と明記する(確定は記録の瞬間=正直設計)。

・入力欄:お名前(text)/メールアドレス(email)/参加人数(number・value=1・min=1)/ひとこと(任意・textarea)。ボタン『申し込む』は送信中 disabled+『送信中…』表示。

・送信は google.script.run.withSuccessHandler().withFailureHandler().saveEntry(data)。成功時は res['状態'] で完了カードを出し分ける:既定=題『受け付けました』本文「ご記入ありがとうございました。確認メールに、お申し込み内容の控え(PDF)を添えてお送りしました。」/'承認待ち'=題『仮受付しました』本文「お申し込みを仮受付しました(主催者の承認をもって確定となります)。\\n確定しましたら、あらためてメールでお知らせします。」/'キャンセル待ち'=題『キャンセル待ちで受け付けました』本文「現在満席のため、キャンセル待ちとして受け付けました。\\nお席が空きましたら、メールでお知らせします。」。res.ok が false のときは alert(res.message)=締切超過・満席・混雑など受け付けられなかった理由をそのまま正直に見せ、ボタンを戻す。通信失敗は withFailureHandler で alert('送信に失敗しました:'+err.message)。

・★このHTMLはテンプレートリテラルで書くため、<script>内のJS文字列の改行は必ず \\n(上の本文例のとおり)。<base target="_top"> を入れる。

────────────────────────────────────────

【章4|状態別の確認メール+控えPDF+Drive保存(五段構えの(2)(3))】

────────────────────────────────────────

■状態別確認メールを送る_(data, 状態, 設定):末尾「_」=非公開(google.script.run から呼べない)。メール未記入なら送らない。三層の独立try/catch:(1)記録は既に済んでいて必ず残る/(2)確認メール(MailApp.sendEmail)はPDF添付の有無に関わらず必ず送る/(3)Drive保存が失敗しても(1)(2)に影響させず握りつぶす。

・件名と本文は状態で出し分け(宛名は「(お名前||'お申し込みの方') 様」、本文にイベント案内文と「このメールは自動送信です。」、主催者名があれば署名として末尾に):

 - 確定:件名『【受付完了】○○ のお申し込みを受け付けました』。本文に「お申し込みを受け付けました。ありがとうございます。お席は確定です。」「控え」PDFを添付した旨、「当日まで念のためお手元に保管ください。」

 - 承認待ち:件名『【仮受付】○○ のお申し込みを受け付けました』。本文に「お申し込みを仮受付しました」「この講座・イベントは承認制のため、主催者の承認をもって確定となります。」「確定しましたら、あらためてメールでお知らせします。」

 - キャンセル待ち:件名『【順番待ちのご案内】○○ はただいま満席です』。本文に「ただいま満席のため、キャンセル待ちとして受け付けました。」「お席が空きましたら、お申し込みの順番にメールでお知らせします。」

・控えPDFは makeEntryPdf(data, 状態, 設定) で1枚だけ作り、メール添付とDrive保存で使い回す(PDFを2回作らない)。PDF化に失敗したら添付なしでメール本体だけ送る。

■makeEntryPdf:Utilities.newBlob(html, 'text/html', 'entry.html').getAs('application/pdf') のGAS内蔵変換(Drive保存もGoogleドキュメントもはさまない=実機で確実に動く書き方)。ファイル名は '申込控え_'+(data.name||'申込者')+'.pdf'。

■entryHtml(data, 受付日時, 状態, 設定):控えの中身。table+インラインstyleだけで組む(背景塗りは描画されないので罫線で表現)。紙面枠640px・題字『申込内容 控え』(下罫線2px)・受付日時(yyyy年M月d日 HH:mm・東京時間)・明細は1項目=1行(左:項目名/右:内容)で イベント/受付区分(=状態)/お名前/メール/参加人数/ひとこと(空なら'(なし)')・フッター注記「このPDFはお申し込み内容の控えとして自動作成されたものです。」。差し込み値は esc() を通す。

■控えフォルダ():DriveApp.getFoldersByName(DRIVE_FOLDER_NAME) の find-or-create(hasNext で既存判定・無いときだけ createFolder)。実行のたびにフォルダが増える事故を防ぐ。

■H列「通知記録」=“もう送った”の覚え書き(この秘書の冪等キー):

・★必ず追記型(カンマ区切り):上書き(setValue一発)にすると、後から書いた札が前の札を消して二重送信防止が壊れる(例:リマインダー済を書いたら繰り上げ済が消える)。だから必ず「読んでから足す」。判定は indexOf(含まれているか)=札の並び順に依存しない。

・通知記録にある(sh, 行番号, タグ)/通知記録に追記(sh, 行番号, タグ)(同じ札は二度貼らない)の2関数。

■行を読む(sh, 行番号):1行を {name, email, count, note} に読み出すヘルパー。

■選択行の行番号():メニュー操作の共通前置き。『申込一覧』シートの上でなければ「『申込一覧』シートの上で、対象の行を選んでから実行してください。」、見出し行・空行なら「見出し行や空の行は選べません。対象の申込の行を選んでください。」と alert して 0 を返す。

────────────────────────────────────────

【章5|承認・お断り・キャンセルと繰り上げ(メニュー操作)】

────────────────────────────────────────

■選択行を承認する():メニュー入口。選択行の行番号() を確かめ、承認を実行_(行番号) の返す報告文を alert。

■承認を実行_(行番号):承認の本体(メニューと管理ページの「確定にする」ボタンが共有=二重実装しない)。末尾「_」=非公開。

・状態が既に「確定」→「この行はすでに「確定」です(何もしていません)。」/「承認待ち」以外→「この行は「承認待ち」ではありません(現在:○○)。承認できるのは承認待ちの行だけです。」で終了。

・G列を「確定」にし、メールがあり H列に「承認確定メール済」が無いときだけ 承認確定メールを送る_ を送り、送れたときだけ札を貼る(失敗時は「状態は「確定」にしましたが、メール送信に失敗しました。お手数ですが手動でご連絡ください。」)。

■承認確定メールを送る_:件名『【確定のお知らせ】○○ のお申し込みが確定しました』。本文「仮受付していたお申し込みが承認され、お席が確定しました。当日のお越しをお待ちしています。」+案内+自動送信注記+主催者名。

■選択行をお断り下書きにする():お断りメールは“自動送信しない”。GmailApp.createDraft(宛先, '【○○】お申し込みについて', 本文) で下書きを作るところまで。本文は「このたびは…お申し込みいただき、ありがとうございました。」「誠に恐れ入りますが、今回はご参加をお受けできない運びとなりました。」「(お断りの理由をここに書き添えてください)」の穴あき文面+主催者名。メール未記入の行は「下書きは作れないため、状態だけ手で「お断り」にしてください。」と案内。下書き作成後にG列を「お断り」にし、alertで「“下書き”をGmailに作りました(自動では送信しません)」「理由の一文を書き添えてから、ご自身で送信してください」と明示。★お断りで席は空くが、キャンセル待ちの自動繰り上げは【しない】(繰り上げが動くのはキャンセル操作だけ。お断り=人の判断の場面なので、次の人を繰り上げるかも人が決める)。この方針をalertにも一言添える。

■選択行をキャンセルにする():メニュー入口。判定と状態変更は saveEntry と同じ ScriptLock(waitLock(10000)・取れなければ「ただいま処理が混み合っています。少し待ってもう一度お試しください。」)の中で キャンセルと繰り上げを実行_(行番号) を呼び(try/finallyでreleaseLock)、報告文を alert。

■キャンセルと繰り上げを実行_(行番号):本体(メニューと管理ページが共有。呼ぶ側がロックを握る)。末尾「_」=非公開。

(1) 既に「キャンセル」なら「(何もしていません)」で終了。選択行をキャンセルにし、SpreadsheetApp.flush() で書き込みを確定させてから数え直す。

(2) 残席を数える(カウンタを持たない主義はここでも同じ)。

(3) キャンセル待ちの先頭(行順=申込順で最初の「キャンセル待ち」行)を探す。いなければ「キャンセルにしました。キャンセル待ちの方はいません(残席○)。」

(4) ★繰り上げの規約=【先頭厳守】:先頭の人数(人数を矯正)が残席に収まるときだけ繰り上げる。収まらなければ、後ろに収まる人がいても繰り上げず、「キャンセルにしました。ただし、キャンセル待ち先頭の方は○人での申込のため、残席○では足りず、繰り上げていません(先頭を追い越しません)。」と正直に報告して終わる(順番の公平を守る。判断が要るときは人がメニューで)。

(5) 繰り上げ:先頭を「確定」にし、H列に「繰り上げ済」が無ければ 繰り上げメールを送る_ を1通。札は“送れたときだけ”貼る(失敗時は札なし=再操作すれば再送できる)。報告文に繰り上げた人の名前・人数と、メールが送れたか(「(繰り上げメール送信済み)。」/「(メールは送れていません。H列を確認してください)。」)を含める。

■繰り上げメールを送る_:件名『【お席のご用意ができました】○○ のお申し込みが確定しました』。本文「キャンセル待ちでお待ちいただいていたお申し込みに、お席のご用意ができました。」「お申し込みは「確定」となりました。」+案内+「ご都合が変わった場合は、お手数ですがこのメールへの返信でお知らせください。」+自動送信注記+主催者名。

────────────────────────────────────────

【章6|前日リマインダーと毎朝まとめ(冪等トリガー)】

────────────────────────────────────────

■トリガーの冪等規約(両機能とも同じ流儀):予約する関数は、先に ScriptApp.getProjectTriggers() から“同じハンドラ関数の予約”を全部消してから1つ作る=何度実行しても予約は常に1つ(2回押したら2通…が仕組みとして起きない)。解約する関数は同じハンドラの予約を全部消して件数を報告。予約・解約とも Session.getEffectiveUser() 宛てに完了メールを1通(実行ログを読みにいかなくても、届けば成功と分かる証拠)。

■前日リマインダーを予約する()/前日リマインダーを解約する():ハンドラは 前日リマインダーを送る。timeBased().everyDays(1).atHour(18)(毎夕18時台=18:00〜19:00のどこか。分単位は指定できない仕様、とコメント)。予約完了メールは件名『【予約完了】前日リマインダー』で、仕組みの説明と「※前日の夕方より後に確定した方には届きません(その方には確認メールが控えです)。」の正直な限界を明記。解約は件名『【解約完了】前日リマインダー(削除した予約:○件)』。

■前日リマインダーを送る():毎夕トリガーが呼ぶ本体。開催日が未設定なら何もしない(静かに見回りだけ)。「開催日の前日か」は、東京時間の yyyy-MM-dd 文字列同士(Utilities.formatDate)で 開催日 と 明日(now+24時間) を比べる=時差や時刻のブレに揺れない判定。前日のときだけ、『申込一覧』を走査して【状態が「確定」/H列に「リマインダー済」が無い/メールあり】の行にだけ1通ずつ送る(三重防御=毎夕1回×前日判定×通知記録)。件名『【前日のご案内】明日は ○○ です』、本文「いよいよ明日の開催です。」+案内+「お気をつけてお越しください。お会いできるのを楽しみにしています。」。1人分の失敗で全員分を止めない(行ごとtry/catch。送れた行にだけ「リマインダー済」を追記=送れなかった行はH列に札が無い=見れば分かる)。送れた件数が1以上なら、LINE送信(章9)で「■ 前日リマインダー」の発送報告を自分へ1通(try/catch・トークン未設定なら中でスキップ)。

■毎朝まとめ:ハンドラは 毎朝まとめを送る、予約は 毎朝まとめを予約する()(everyDays(1).atHour(7)=毎朝7時台)、解約は 毎朝まとめを解約する()。予約完了メール件名『【予約完了】毎朝の申込まとめ(申込・予約秘書)』には「※基礎編の受付ページでも毎朝まとめを予約している場合は、毎朝2通届きます。」の注記。

■毎朝まとめを送る():きのう0:00〜きょう0:00 の1日分を まとめメールを送る(開始, 終了, 期間ラベル) へ。動作確認用に テスト_まとめを今すぐ送る()(きのう0時〜いま)も用意。

■まとめメールを送る(開始, 終了, 期間ラベル):宛先=Session.getEffectiveUser()(自分)。期間内の申込行(A列がDateの行だけ・開始<=受付<終了)を拾い、件名『【申込まとめ】○○ 期間ラベル:○件』で1通。本文に:期間/申込件数(参加人数の合計)/状態内訳(確定・承認待ち・キャンセル待ち・キャンセル・お断り、想定外は(その他))/いまの残席(定員)/明細(1件=「・M/d HH:mm お名前(○人)【状態】〔AI仕分け〕」+ひとことがあれば2行目)。★メールアドレス列はまとめに載せない(毎朝のメールに個人情報を増やさない)。0件の日も「この期間の申込は0件でした。」を送る=無音だと“申込ゼロ”と“故障”の区別がつかない。毎朝届くこと自体が生存報告、という一文を入れる。日付ラベル(d)(M月d日(曜))・日時ラベル(d)(M/d HH:mm)のヘルパーも。

■シートを整える():見出しの固定・太字・状態プルダウンを確保・autoResizeColumns(1,8) を整え直すだけ(データは1文字も触らない)。alertで報告。

────────────────────────────────────────

【章7|管理ページ(育てる編④・本人ガード・google.script.run)】

────────────────────────────────────────

同じコードの【第2デプロイ(アクセス=自分のみ)】で ?page=admin を開く想定。公開ページには触らない。どちらのURLで開かれても、doGet側の 管理者である() が二重ガード(主催者本人=シートのオーナー以外には中身を見せない)。

■管理ページのURLを表示():ScriptApp.getService().getUrl() に '?page=admin' を付けて alert。未デプロイなら「まだWebアプリとしてデプロイされていません。先に「デプロイ」→「新しいデプロイ」(ウェブアプリ)を行ってください。」。「※管理ページ用には「アクセスできるユーザー=自分のみ」の第2デプロイを作るのがおすすめ」と「どちらのURLで開いても、主催者本人(このシートのオーナー)以外には中身を見せません」を案内文に含める。

■管理ページHtml():スマホで見やすい1カード。中身は:イベント名+『管理ページ』の見出し/「このページは主催者専用です。ボタンの操作はシートのメニューと同じ処理を通ります。」の注記/残席メーター(使用=定員−残席・負なら0にクランプ。バー幅=定員>0のとき Math.min(100, Math.round(使用*100/定員))・定員0なら0=0除算・100%超え・負値の3ガードで初期化直後や定員0でも壊れない。ラベル「定員 ○人中 ○人(確定+承認待ち)/残席 ○」)/申込一覧テーブル(受付日時・お名前・人数・状態・操作。申込0件なら「まだ申込はありません。」)。各行に『確定にする』『キャンセルにする』のボタンを置き、data-fn(管理_確定にする/管理_キャンセルにする)と data-row(行番号)を持たせ、confirm→google.script.rundata-fn で“同じ本体”を呼ぶ(メニューと二重実装しない)。成功で alert+location.reload()、失敗で alert+ボタン復帰。差し込み値は esc()。

■管理_確定にする(行番号):最初に 管理者である() を確かめ、違えば throw new Error('この操作は主催者専用です。')。行番号を parseInt で検証(2〜最終行の外は「対象の行が見つかりません。」)。あとは 承認を実行_(行番号) を呼ぶだけ。

■管理_キャンセルにする(行番号):同じ本人確認+行検証のうえ、ScriptLock(waitLock(10000))を取り try/finally で キャンセルと繰り上げを実行_(行番号) を呼ぶ(メニューと同じ繰り上げ処理を共有)。

(この2つは管理ページの google.script.run から呼ぶ正規の入り口なので末尾「_」を付けない。代わりに冒頭の本人確認が門番。)

────────────────────────────────────────

【章8|AI仕分け(Gemini・任意=キー未設定なら静かにスキップ)】

────────────────────────────────────────

■定数 var GEMINI_MODEL = 'gemini-2.5-flash'; とヘルパー Geminiキー()(PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY')。キーはコードに直書きしない=コードを人に見せても漏れない。設定場所[プロジェクトの設定→スクリプト プロパティ]をコメントで案内)。

■AI仕分けして記録(sh, 行番号, data):ひとこと未記入なら、AIを呼ぶまでもなくF列(6列目)に「なし」(無料枠も使わない)。ひとことをAIで仕分け(note) の答えをF列に書き、答えが「質問」のときだけ 質問アラートを送る(data, note) を独立try/catchで。AIが答えられなかったら空欄のまま(=あとで見返せば分かる正直な状態)。

■ひとことをAIで仕分け(note):キー未設定なら null(受付そのものは通常どおり動く)。プロンプトは「あなたはイベント受付の係です」+選択肢3つ=質問(返事が必要な問いかけがある)/要望(お願い・相談・配慮のリクエスト)/あいさつ(感想・意気込み・挨拶など、返事は不要)+「迷ったら:返事が必要そうなら「質問」を優先」+出力は {"種類":"質問"} のJSONだけ。呼び方は v1beta の generateContent に UrlFetchApp でPOST、キーはヘッダー x-goog-api-key(URLに載せない)、generationConfig は responseMimeType:'application/json' と thinkingConfig:{thinkingBudget:0}(考え込まず即答=申込者を待たせない)。muteHttpExceptions:true。★リトライは粘らない:最大2回まで=混雑(429/5xx)は“1回目のときだけ”2秒待って取り直し、2回目の失敗やキー誤り(4xx)は待たずに諦めて null(記録・メール・控えは既に済んでいて、失って困るものはない)。返答は “`json を除いて JSON.parse し、答えが3語のどれかのときだけ採用(想定外の答え・いたずら文への保険)。

■質問アラートを送る(data, note):宛先は Session.getEffectiveUser().getEmail()(※getActiveUser だと、ログインしていない申込者が送信した瞬間の実行では空文字になるため使わない、とコメント)。件名『【返事が要る申込】○○ さんから質問が届いています』、本文に申込内容(お名前・メール・参加人数・ひとこと)と「※この仕分けはAIによる自動判定です。たまに読み違えることがあります。」。

────────────────────────────────────────

【章9|LINE通知(任意=トークン未設定なら静かにスキップ)】

────────────────────────────────────────

■LINEトークン():PropertiesService.getScriptProperties().getProperty('LINE_TOKEN')(Geminiキーと同じ持ち方=直書きしない)。

■申込をLINEに知らせる(data, 状態, 設定):申込1件を短い1通に整えて送る。1行目「■ 新しい申込【状態】」=状態の札つき(満席間際の「キャンセル待ち」もポケットで分かる)、イベント名・お名前(○人)・ひとこと(あれば)・受付時刻。★メールアドレスはLINEに載せない(ポケットに個人情報を増やさない。詳細はシートと毎朝まとめにある)。

■LINE送信(text):トークン未設定なら静かに何もしない(この秘書の核はメールだけで完全に動く)。https://api.line.me/v2/bot/message/broadcast へ Authorization: Bearer で1通(この公式アカウントの友だちは自分ひとり=実質“自分専用の通知回線”)。★失敗しても粘らない(取り直しなし)。200以外は Logger.log に code と本文先頭を残す(401=トークン誤り/429=今月の無料配信枠(200通)切れ の見当がつくように)。muteHttpExceptions:true。

(コメントに:既存チャネルを流用すると無料配信枠200通/月も共有される=使うなら秘書専用の新チャネル推奨。)

────────────────────────────────────────

【章10|動作確認用のテスト関数(すべて自分あて・架空名)】

────────────────────────────────────────

本番の申込者には1通も飛ばないテストを3つ:

・テスト_申込を1件入れる():saveEntry({ name: 'テスト 太郎', email: Session.getActiveUser().getEmail(), count: '1', note: '会場に駐車場はありますか?' }) を呼び、結果をLogger.logへ。五段構え+状態決定を全部通す(成功のしるし:シートに1行+状態/確認メール+控えPDF/Driveの『申込・予約秘書_控え』に控え/AI仕分け列に札(キー設定時)/LINEに1通(トークン設定時))。

・テスト_残席1に2人申込():saveEntry({ name: 'テスト 花子', email: 自分, count: '2', note: '2人での参加を希望します' })。“残席1に2人”の入口規約を机上で確かめる(定員をいまの「確定+承認待ち+1」にしてから実行すると残席1になる。その状態で「キャンセル待ち」または「残席が足りません」になれば規約どおり、という使い方をコメントに)。

・テスト_LINE接続():LINE_TOKEN 未設定なら設定手順を throw で案内。設定済みなら LINE送信 で接続テスト文を1通(シートにテスト行を作らない)。

以上の仕様を満たす完成コードを、コピーして貼ればそのまま動く形で出力してください。

“`

実際にやってみる = スマホで3件、申し込んでみた

ここからが、今日の山場です。設定は、わざと小さくしました。定員2・締切は明日の18時。 満席を、自分の目で見るためです。

1件目。 スマホで受付ページを開くと、イベント名と開催日時の案内の下に、「残り2席(目安)」と出ています。お名前・メールアドレス・参加人数・ひとことを入れて「申し込む」。ボタンが「送信中…」に変わって、数秒で「受け付けました」

スマホの申込フォーム。イベント名と開催日時の案内の下に「残り2席(目安)」の表示と入力欄

受信箱には、確認メールが届いていました。件名は「【受付完了】テスト講座 のお申し込みを受け付けました」。本文には「お席は確定です」と明記され、申込内容をまとめた控えPDFが添付されています。基礎編で組んだ五段構え——記録、確認メール+控えPDF、Drive保存——が、この秘書の中でもそのまま生きています。

受信箱の確認メール。件名【受付完了】、本文「お席は確定です」、申込控えPDFの添付つき

2件目。 ページを開き直すと、表示は「残り1席(目安)」に減っています。同じように申し込んで、「受け付けました」。これで定員ちょうど、満席です。

3件目。 満席になったことを、誰にも知らせていません。フォームも閉じていません。その状態で、3人目としてもう一度URLを開くと——

3件目のアクセスで表示された満席画面。フォームの代わりに「満席となりました」のカードだけが出る

フォームが、出てきません。 入力欄も、「申し込む」ボタンもなし。「満席となりました」のカードが1枚あるだけです。3人目は、申し込もうにも申し込めない。定員オーバーは、入り口で起きなくなりました。

あとで実行ログ(サーバ側の記録)を確かめると、これがただの見た目の芸ではないことが分かります。1件目と2件目は、申込を記録する処理がきちんと走った記録が残っている。ところが3件目は、ページを表示する処理しか動いていません。 満席と判断した時点で、申込を受け取る仕事そのものに進まない——画面とサーバの両方で、扉が閉まっていました。

締切は、セル1つで実演できる

もうひとつの「閉まる」も見ておきます。『設定』の締切日時のセルを、昨日の日付に書き換える。それだけです。コードは触らない、公開し直しもしない。

書き換えたあと、スマホで同じURLを開くと——

受付は終了しました。
たくさんのお申し込みをありがとうございました。

締切日時を過ぎたあとの同じURL。「受付は終了しました。」のカードに切り替わる

夜の23時すぎ、布団に入る前のスマホで、この画面が出たときは少し感動しました。セルをひとつ書き換えただけで、公開中のページの挙動が変わる。 基礎編では「本流に手を入れたら公開し直し」と何度も唱えてきたのに、今回はなぜ平気なのか。種は単純で、この秘書はページを開かれるたびに、設定シートを読み直してから返事を決めるからです。コードは変わっていない。変わったのは、コードが毎回読みに行く「メモ」のほうだけ。だから、育てる編の約束——「再デプロイしません」——が成り立ちます。

つまずき帳:「申し込む」ボタンが、押しても何も起きなかった

順調に見えるでしょう。でも、build in publicの約束どおり、途中で起きたことも正直に書きます。じつは最初の実機テストで、この秘書はうんともすんとも言いませんでした。

スマホでフォームは、きれいに表示される。入力もできる。ところが「申し込む」を押しても、何も起きないんです。「送信中…」にすら変わらない。エラーも出ない。ただの無反応。サーバ側の実行ログを見ると、申込を受け取る処理は一度も呼ばれていませんでした。送信が、届いてすらいない。

AIと一緒に原因を探して、たどり着いた真犯人は、コードの中のたった2ヶ所の「改行の書き方」でした。

非エンジニア向けに、たとえで説明させてください。このフォームの画面は、サーバが「台本」を印刷してブラウザに渡す作りになっています。台本のセリフの中には「ここで改行」という記号(\n)を書いておく場所があるのですが、今回の印刷方式では、その記号が印刷の時点で本物の改行に化けてしまったんです。結果、ブラウザに届いた台本では、セリフが1行の途中でぶつ切れになっていた。ぶつ切れの台本を渡された役者(ブラウザ)は、行儀よくその台本のページを丸ごと読むのをやめます。読まれなかったページには、「申し込むボタンが押されたら送信する」という肝心の段取りが書いてありました。だからボタンは、押されても誰も応えない——無反応だったわけです。

こわいのは、この間違いが機械のチェックをすり抜けたことです。コード全体の文法チェックは合格していました。台本の「地の文」は正しくて、「セリフの中身」だけが壊れていた。こういうものは、実際にスマホで押してみるまで分かりません。

だから、教訓はシンプルです。テスト関数が通っても、本番の1回は必ず自分のスマホで申し込むこと。 どんなに立派な受付ページも、「申し込む」が押せなければ、ただの綺麗な貼り紙です。

なお、正直な補足をひとつ。この修正のために、わたしは開発中に一度だけコードを貼り直し、公開し直しをしています。あなたがこの記事のプロンプトから受け取るコードは、退治済みのものです。「貼り直しも再デプロイも、もうしません」という約束は、この完成形を貼ったあとの話——そこは、ずるくないように書き添えておきます。

裏側のしくみ(ここは読み飛ばしてOK)

ここは、仕組みに興味がある人だけ、のぞいてください。読み飛ばしても、秘書はちゃんと動きます。

  • 窓口は、一度にひとりです。 もし残り1席に、2人がまったく同時に申し込んだら? 両方に「受け付けました」と言ってしまったら定員オーバーです。この秘書は、申込を記録する窓口に「順番待ちのロープ」(LockServiceという仕組み)を張っていて、必ずひとりずつしか通しません。 ひとり目の記録が終わってから、ふたり目の判定が始まる。だから、同時押しでも席は二重に売れません。
  • 残席は、毎回数え直します。 「残りあと何席」というカウンタをどこかに持っておくと、いつかカウンタと現実がずれます(ずれたカウンタほど怖いものはありません)。だからこの秘書はカウンタを持たず、判定のたびに申込一覧を最初から数え直します。 台帳が唯一の真実、という考え方です。
  • 最終判定は、記録の直前にやります。 フォームに出ている「残り○席(目安)」は、ページを開いた瞬間の値です。開いたまま10分悩んでいるあいだに、席は埋まるかもしれない。だから本当の判定は、「申し込む」が押されて、記録する直前に、ロープの内側でもう一度やります。締切も同じで、直前にもう一度確かめます。画面は目安、確定は記録の瞬間。 ここを混ぜないのが、この秘書の背骨です。
  • 席が足りないときは、受けません。 残り1席のところに「2人で参加」の申込が来たら、1人だけ受ける——ようなことはせず、「残席が申込人数に足りません」と正直に断ります。中途半端に受けるほうが、あとで揉めるからです。
  • 判定が終わったら、ロープを外してから残りの仕事をします。 確認メールを書いたりPDFを作ったりは、数秒かかる仕事です。それを窓口のロープの内側でやると、後ろの人を待たせてしまう。だから、席の判定と記録だけをロープの内側で済ませ、メールやPDFはロープを外してからゆっくり作ります。基礎編と同じく、これらの段はそれぞれ独立していて、どれかが転んでも申込の記録は無傷です。

秘書は、もう全部持っています = メニュー全景と、これからの育てる編

最後に、メニューを開いた全景をお見せします。今日使ったのは、いちばん上の「初期化(シートを作る)」だけ。その下に、見慣れない項目がずらりと並んでいるのが分かるでしょうか。

スプレッドシートのカスタムメニュー「申込・予約秘書」。全項目が最初から並ぶ

「選択行を承認する」「選択行をキャンセルにする(繰り上げつき)」「前日リマインダーを予約する」「管理ページのURLを表示」——これらはぜんぶ、今日貼ったコードの中に、もう入っている道具です。これから1本ずつ、育てる編で開けていきます。

  • 育てる編①「満席のあとに、列を作る」:『設定』の【キャンセル待ち】をONにするだけで、満席後の申込が“列”に並び、キャンセルが出たら先頭の人が自動で繰り上がる
  • 育てる編②「申込と確定のあいだに、人の判断を挟む」:【承認制】をONにするだけで、申込は「仮受付」で止まり、あなたの承認で確定する。お断りのメールは自動送信せず、下書きまで
  • 育てる編③「当日前夜、確定した人みんなに前日リマインダー」:メニューで“予約”を1回押すだけ
  • 育てる編④「ポケットに管理画面」:受付ページと同じURLの中に、主催者本人だけが中を見られるようにしたもう1つの入り口を用意してあります(開き方と仕組みは、④でくわしく)。残席メーターと、確定・キャンセルのボタン。公開ページには触りません

そして、約束をもう一度。どの回でも、コードの貼り直しも、公開ページのデプロイし直しも、しません。 変わるのは、設定のセルと、押すメニューだけです。

正直にできないこと(先に言っておきます)

このブログのやり方として、できないことは先に正直に言います。この秘書にも、はっきりした限界があります。

  • フォームの残席表示は、“目安”です。 表示されるのはページを開いた瞬間の値で、席が確定するのは記録の瞬間です。だから画面にも「(目安)」と正直に書いてあります。開いているあいだに埋まれば、送信時に満席のご案内になります。
  • キャンセルの受付は、人がやります。 申込者が自分で操作するキャンセルページは、作っていません。「行けなくなりました」の連絡はメールなどで受けて、あなたがメニュー(またはこのあと登場する管理ページ)から操作する形です。連絡を受けて手を動かすところは、人の仕事として残してあります。
  • 1シート=1イベントです。 この台紙で同時にさばけるのは、ひとつの催しだけ。複数のイベントを並行して受け付ける機能はありません。次の募集は、同じシートを使い回す前提です(その手順も、いずれ育てる編で)。
  • 時間枠を選ぶ予約(三者面談型)は、守備範囲外です。 「7月13日の10時の枠/10時半の枠…」のようなスロット予約は、この秘書の設計に入っていません。あれは席の数え方がまるで別物なので、無理に載せず、いつか別の秘書として考えます。
  • メールは、軽重で線を引いています。 確認メールやご案内は自動で送ります。でも、お断りのような相手の心情に関わる重いメールは、自動送信しません——Gmailの下書きを作るところまでで、最後に読んで送信を押すのはあなたです(育てる編②)。また、育てる編③の前日リマインダーが届くのは「前日の夕方までに確定した人」。それより後に確定した人には、確認メールが控えになります。

——受付を任せる相手が、こうして「ここからは目安です」「ここからは人の仕事です」と正直に申告してくれること。それ自体が、いちばん信頼できるところだと、今回も思っています。

次回予告:育てる編① = 満席のあとに、列を作る

満席の画面が出るようになった——それは裏を返すと、満席のあとに来てくれた人を、そのまま帰してしまうということでもあります。「キャンセルが出たら、ぜひ入りたいのに」という人の席は、まだありません。

次回は、『設定』シートの【キャンセル待ち】をONにするだけで、その人たちの“列”ができるようになります。満席後の申込は「キャンセル待ち」として静かに並び、キャンセルが出たら、列の先頭の人が自動で繰り上がって、お知らせのメールが飛ぶ。誰を繰り上げるかで悩む夜も、秘書が引き受けます。

もちろん、コードは貼り直しません。列を作る道具は、今日あなたが貼ったコードの中に、もう入っています。ここまで手を動かせたあなたなら、次はセルをひとつONにするだけです。


※この記事の操作はすべて無料枠の範囲で行っています。画面のお名前・メールアドレス・各種ID/URLは、マスクまたは架空のサンプルに差し替えています。画面の表示や許可画面の文言は時期や環境により変わることがあるため、最新の内容はご自身の環境でご確認ください。コードは、一行も書いていません。

#AI×GAS #応用編その4 #申込予約 #受付システム #非エンジニア #つまずき帳

タイトル案(3つ)

  1. 満席になった瞬間、受付ページが自分から閉まる|申込・予約 秘書【応用編その4】
  2. 「受付システムが完成しました」とは言いませんでした——あれは予告でした|申込・予約 秘書【応用編その4】
  3. 定員と締切は、セルに書くだけ|満席も受付終了も、ページが自分で判断する【応用編その4】

コメント

タイトルとURLをコピーしました