店舗別ログ・日報PDF出力・軽量化まで実装した移動スーパー日報アプリ
✅ 今回作ったもの
今回は、移動スーパーの日報入力をスマホから行える GAS Webアプリ として作り直しました。
現場担当者は、スマホで店舗・担当者・コースを選び、販売場所ごとに到着、客数、備考、出発を記録します。
管理者は、Googleスプレッドシート上のメニューから日報更新やPDF出力を行います。
全体の流れは次のようなものです。
店舗選択
↓
担当者選択
↓
本日のコース確認
↓
拠点ごとに到着・客数・備考・出発を記録
↓
店舗別ログに保存
↓
管理者メニューから日報更新
↓
店舗別PDFを出力
単に入力フォームを作ったというより、
現場入力 → 店舗別ログ → 日報更新 → PDF出力 までをつなげた日報アプリです。
一番大きかったのは、以前制作したLINE BotをGAS Webアプリに置き換えたことだけではありません。
最初は、入力すれば日報まで自動で作成される形を目指していました。
しかし、実際に使ってもらうと、現場入力時に重い処理を走らせると誤操作につながることが分かりました。
そのため最終的には、現場入力時はログ保存と状態更新だけに絞り、日報更新やPDF出力は管理者メニューに分ける設計にしました。
今回のプロトタイプでできたことは、主に次の通りです。
| 分類 | 内容 |
|---|---|
| 現場入力 | スマホから店舗・担当者・コースを選択し、到着・客数・備考・出発を記録 |
| 状態管理 | 現在の拠点、到着済み、休憩中などを保持 |
| ログ保存 | 店舗別ログシートへ記録 |
| 管理者処理 | 日報更新、シート整形、PDF出力をメニューから実行 |
| 軽量化 | 現場入力時は重い処理を走らせない |
🚚 前提:移動スーパーの1日の流れ
移動スーパーは、店舗で商品を準備し、車両に商品を積み込み、決まった販売場所を順番に回って販売します。
販売が終わったら店舗へ戻り、その日の記録を日報として残します。
流れを簡単に書くと、次のようになります。
店舗で準備
↓
商品を積み込む
↓
出発
↓
販売場所A・B・Cを回る
↓
店舗へ戻る
↓
日報を残す
移動スーパーでは、1日に複数の販売場所を回ります。
そのため、日報では単に「今日いくら販売しました」と残すだけでは足りません。
販売場所ごとに、次のような情報を残す必要があります。
| 残したい情報 | 理由 |
|---|---|
| どの販売場所でいくら販売したか | 拠点ごとの実績を確認するため |
| 何時に到着・出発したか | 販売時刻を管理するため |
| それぞれの販売場所で客数は何人だったか | 拠点ごとの利用状況を見るため |
| 備考・申し送りはあったか | 次回運行時に先週の状況を確認するため |
| 販売スキップなどがあったか | 通常運行と違う動きを残すため |
紙の日報でも、ある程度の記録はできます。
しかし、あとから販売場所ごとに比較したり、売上データや商品データと紐付けたりするには扱いづらくなります。
そこで、日報入力をスマホ化し、販売場所ごとの情報をデータとして残すことを考えました。
📌 アプリでやりたかったこと
今回やりたかったことは、紙の日報をスマホ入力に置き換えることだけではありません。
もちろん、現場で日報を入力しやすくすることは大事です。
ただ、それだけなら、入力フォームを作るだけでもある程度できます。
今回もっと重視したのは、販売場所ごとのデータを残し、あとから活用できる形にすることです。
📝 備考を残す理由
移動スーパーでは、販売場所ごとに状況が違います。
たとえば、前回の販売時に次のようなことがあったとします。
今日はお客さまが少なかった
次回は少し早めに来てほしいと言われた
この場所ではこの商品がよく動いた
販売場所の状況が変わっていた
近隣で工事があり、停車場所に注意が必要だった
こうした情報は、次回運行時にとても重要です。
しかし、紙の日報や担当者の記憶だけに頼ると、次回の販売前に確認しづらくなります。
担当者が変わった場合は、なおさら情報が引き継がれにくくなります。
備考を残す目的は、単なるメモではありません。
先週の状況を確認し、次回の対応を修正できるようにすること。
これが、備考・申し送りを日報アプリに入れた理由です。
📊 日報データを蓄積する理由
日報で、次の情報を正確に残します。
どの店舗が
どのコースで
どの販売場所に
何時に到着し
何時に出発し
客数が何人で
どんな備考があったか
これらを蓄積することで、正確な販売時刻管理データを作れます。
この日報データを販売Rawデータや商品別実績と紐付けることで、販売場所ごとのデータに変換が可能になります。
社内システムだけでは、店舗全体や日別の売上は確認できても、
販売場所一か所ごとの商品傾向や売上単価 までは見えにくい場合があります。
しかし、日報側で販売場所・販売時刻・客数を正確に持っておけば、売上データや商品データと組み合わせることで、次のようなことが見えるようになります。
販売場所ごとの商品傾向
販売場所ごとの売上
販売場所ごとの客単価
曜日ごとの売れ方
販売時間と売上の関係
客数と売上単価の関係
これが見えるようになると、日報は単なる記録ではなくなります。
商品の持ち出し数の見直し、当日の販売予測、コースの見直しや改善に使えるデータになります。
🧭 日報データ活用のイメージ
今回のアプリは、紙の日報をスマホ入力へ置き換えるだけでなく、蓄積したデータを段階的に業務改善へ活用することを目指しています。
現在できていることと、今後取り組みたいことを3つのフェーズに分けて整理しました。
| フェーズ | 取り組むこと | 内容 | 現在の状況 |
|---|---|---|---|
| フェーズ1 | 現場入力のデジタル化と効率化 | スマホから到着時刻・出発時刻・客数・備考を入力し、店舗別ログへの保存、日報更新、PDF出力まで行う | 実装済み |
| フェーズ2 | データの蓄積と現状分析 | 販売場所ごとの時刻・客数・備考を蓄積し、拠点ごとの利用状況や運行実績を比較できるようにする | データ蓄積まで実装。分析は今後 |
| フェーズ3 | データによる予測とコース改善 | 売上データや商品別実績と組み合わせ、販売予測、商品の持ち出し数、訪問順、販売場所の見直しに活用する | 今後の構想 |
フェーズ1:現場入力のデジタル化と効率化
今回のプロトタイプでは、次の流れまで実装しました。
紙の日報・担当者の記憶
↓
スマホで日報入力
↓
販売場所ごとの時刻・客数・備考を記録
↓
店舗別ログへ保存
↓
管理者が日報を更新
↓
店舗別PDFを出力
現場担当者はスマホから必要な情報を入力し、管理者はスプレッドシート上のメニューから日報更新やPDF出力を行います。
現場入力時には重い処理を行わず、ログ保存と状態更新だけに絞ることで、操作しやすさを優先しました。
フェーズ2:データの蓄積と現状分析
日報には、次の情報を販売場所ごとに蓄積します。
- 到着時刻
- 出発時刻
- 滞在時間
- 客数
- 備考・申し送り
- スキップなどの通常と異なる運行
これらのデータが蓄積されれば、販売場所ごとの客数、滞在時間、遅れやすい区間、備考の傾向などを比較できるようになります。
現時点ではデータを蓄積できる仕組みまで作成しており、今後は集計や分析方法を整えていきます。
フェーズ3:データによる予測とコース改善
将来的には、日報データを販売Rawデータや商品別実績と組み合わせます。
これにより、次のような活用を想定しています。
- 販売場所ごとの売上や客単価を確認する
- 販売場所ごとの商品傾向を把握する
- 実績に合わせて商品の持ち出し数を見直す
- 当日の販売予測に活用する
- 遅れが発生しやすい訪問順を見直す
- 販売場所やコース編成の改善に活用する
今回のアプリは、日報作成を効率化するだけでなく、販売場所ごとのデータ活用につなげるための土台として作成しています。
🧭 アプリの全体像
現場担当者が使うのは、スマホ用のWebアプリです。
管理者が使うのは、Googleスプレッドシート上の管理者メニューです。
現場担当者
↓
スマホでGAS Webアプリを開く
↓
店舗・担当者・コースを選択
↓
拠点ごとに到着・客数・備考・出発を記録
↓
店舗別ログに保存
↓
管理者が日報更新
↓
店舗別PDFを出力
技術要素としては、次のような構成です。
| 技術要素 | 役割 |
|---|---|
| GAS Webアプリ | スマホ入力画面 |
| Googleスプレッドシート | マスタ・状態管理・ログ保存 |
| Google Apps Script | 入力処理、状態更新、日報更新、PDF出力 |
| LockService | 同時保存対策 |
| カスタムメニュー | 管理者による日報更新・PDF出力 |
| Google Drive | 出力PDFの保存先 |
現場担当者は、スプレッドシートを直接編集しません。
スマホ画面から、決められた順番で入力します。
管理者は、スプレッドシート上のメニューから日報更新やPDF出力を行います。
このように、現場入力と管理者処理を分けることで、現場画面をシンプルにしつつ、管理者側では日報として見返せる形にしました。
📱 画面イメージ
店舗選択画面
店舗を選ぶことで、担当者やコースの候補を絞り込みます。
担当者選択画面
担当者はマスタから選択します。
固定担当者以外が運行する場合に備えて、「その他」も選べるようにしました。
コース確認画面
曜日に応じて、その日のコースを自動で表示します。
拠点入力画面
現在の拠点を表示し、到着・客数入力・備考・出発を記録します。
管理者メニュー
管理者は、スプレッドシート上のメニューから日報更新やPDF出力を行います。
ここまで見ると、最初からGAS Webアプリを作ろうとしていたように見えるかもしれません。
でも、最初に作っていたものはLINE BotとMakeを使った日報入力Botでした。
🔁 最初はLINE Bot + Makeで作っていた
今回のGAS Webアプリ版を作る前に、前回はLINE BotとMakeを使って、移動スーパーの日報入力Botを試作していました。
前回の記事はこちらです。
構成としては、LINEで入力された内容をMakeで受け取り、Googleスプレッドシートへ記録する形です。
LINE Bot
↓
Make
↓
Googleスプレッドシート
最初にLINE Botを選んだ理由は、現場担当者が感覚的に使える入口にしたかったからです。
移動スーパーの販売担当者は、中年以上の担当者も多く、業務用の新しいツールやアプリに慣れていない人もいます。
そのため、最初から専用アプリや複雑な入力画面を使ってもらうよりも、普段から使っているLINEのトーク画面で入力できる方が、心理的なハードルが低いと考えました。
LINEなら、表を直接編集したり、専用画面を探したりせず、Botの案内に沿って順番に入力できます。
ツールに慣れていない人でも、感覚的に使える入口にしたかったのが、最初にLINE Botを選んだ理由です。
Make側では、状態管理シートを参照しながら、入力内容に応じて処理を分岐していました。
ただ、継続運用を考えると、Makeのクレジット消費やシナリオの複雑化が気になりました。
💬 チームから出た指摘
LINE Bot + Make版を共有したところ、チームから次のような指摘がありました。
・LINEでずっと運用できるのか?
・Makeを使い続ける場合、クレジット消費や料金面は大丈夫か?
・Googleアンケートのような形でも入力できるのではないか?
この指摘を受けて、一度ツール選定から考え直すことにしました。
| もらった指摘 | 気づいたこと | 変更したこと |
|---|---|---|
| LINEでずっと運用できるのか? | 入力の入口だけでなく、継続運用も考える必要がある | GAS Webアプリ案を検討 |
| Makeのクレジット消費は大丈夫か? | 利用台数が増えると外部ツール依存が弱点になる | Make依存を減らす方向へ |
| Googleアンケートでもよいのでは? | 一方通行の入力では日報業務に足りない | 状態管理できるWebアプリへ |
💡 Makeの「クレジット」とは?
Makeでは、シナリオを実行すると、その処理内容に応じて クレジット を消費します。
ざっくり言うと、Make上で自動化処理を動かすたびに使う利用単位のようなものです。
今回のように、日報入力を毎日・複数店舗・複数車両で使う場合、実行回数が増えるため、無料枠だけで継続運用するのは難しくなる可能性があります。
Make自体が悪いということではなく、継続運用する業務アプリとして考えたときに、クレジット消費と料金面を無視できませんでした。
🧾 Googleフォームでは足りなかった
Googleフォームで入力する案もありました。
ただ、移動スーパーの日報業務には合いませんでした。
一方通行の入力では、業務に使えないからです。
日報は、1回送信して終わるアンケートではありません。
到着を記録したら、次は客数入力。
出発したら、次の拠点へ進む。
途中で休憩や担当者変更もあります。
前回の申し送りを確認して、元の運行画面に戻ることもあります。
必要だったのは、回答フォームではなく、現在の状態に応じて画面が変わる仕組みでした。
🔧 GAS Webアプリとして作り直すことにした
LINE Bot + Makeには継続運用面の課題があり、Googleフォームでは状態管理が足りませんでした。
そこで、Google Apps Scriptを使って、Webアプリとして一から作り直すことにしました。
GAS Webアプリであれば、Googleスプレッドシートをデータベースのように使いながら、スマホ向けの入力画面を作れます。
今回GAS Webアプリにした理由を整理すると、次の通りです。
| 理由 | 内容 |
|---|---|
| Google環境で完結しやすい | スプレッドシート、Apps Script、Driveを使える |
| 画面を自由に作れる | スマホ向けのボタン・入力画面を作れる |
| 状態管理ができる | 現在の拠点、休憩中、業務終了などを管理できる |
| 日報出力につなげられる | スプレッドシート上で日報更新やPDF出力ができる |
| 外部ツール依存を減らせる | Makeや他の自動化サービスへの依存を減らせる |
| 将来のデータ活用につなげやすい | 店舗ID、コースID、拠点順などを持たせられる |
LINE Botを改修するのではなく、GAS Webアプリとして最初から組み直すことにしました。
🔧 Webアプリ画面を表示する doGet() のコードを見る
function doGet() {
return HtmlService
.createHtmlOutputFromFile('index')
.setTitle('移動スーパー日報アプリ')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
この index.html 側に、店舗選択や担当者選択、到着・出発ボタンなどの画面を作ります。
GAS側では、スプレッドシートから店舗マスタ、担当者マスタ、コースマスタ、状態管理、店舗別ログを読み書きします。
🔧 画面側からGAS関数を呼び出すコードを見る
google.script.run
.withSuccessHandler(function(result) {
setSaving(false);
showCurrentStop(result);
})
.withFailureHandler(function(error) {
setSaving(false);
alert(error.message || error);
})
.recordDeparture(customerCount, remark);
画面側で客数や備考を入力し、出発ボタンを押すと、GAS側の recordDeparture() を呼び出します。
GAS側では、入力内容をログに保存し、状態管理を更新し、次の画面表示に必要なデータを返します。
この時点で、作るべきものはフォームではなく、状態を持って進むWebアプリだと考えました。
そこで、GAS Webアプリとして一から作り直すことにしました。
🧩 最初のプロトタイプ:日報まで自動で作る形にした
GAS Webアプリとして作り直すなら、現場担当者が入力するだけで、日報まで自動でできる形にしたいと考えました。
現場担当者がスマホから入力する。
その内容がログに残る。
日報シートにも反映される。
店舗別サマリーも更新される。
申し送り一覧も作られる。
ここまで自動でできれば、管理者側の手作業もかなり減らせます。
最初に考えていた流れは、次のようなものでした。
現場担当者が入力
↓
記録シートへ保存
↓
状態管理を更新
↓
日報_拠点別を書き換え
↓
日報_店舗別サマリーを書き換え
↓
申し送り一覧を書き換え
↓
必要に応じてシート整形
↓
次の画面へ進む
この形は、一見かなり便利です。
入力すれば、自動で日報までできる。
管理者があとから加工しなくても、すぐに確認できる。
自動化としては、とても分かりやすい形です。
最初は、これが理想形だと思いました。
✨ デザインも整えて、かなり良い形に見えた
最初のプロトタイプでは、画面の見た目も整えました。
スマホで使うことを前提に、ボタンを大きくし、現在の店舗、担当者、コース、拠点が分かりやすいようにしました。
現場担当者が迷わないように、画面にはできるだけ必要な情報だけを表示します。
今どの店舗か
誰が担当しているか
今日のコースは何か
現在の拠点はどこか
次に押すボタンは何か
店舗を選び、担当者を選び、コースを確認し、到着・客数・出発を記録する。
日報も自動で作られる。
この時点では、かなり良いものができたように見えました。
🗂 スプレッドシート設計
GAS Webアプリの裏側では、Googleスプレッドシートをデータベースのように使っています。
主なシートは次の通りです。
| シート名 | 役割 |
|---|---|
| 店舗マスタ | 店舗情報を管理 |
| 担当者マスタ | 担当者情報を管理 |
| コースマスタ | 曜日ごとのコース・拠点を管理 |
| 状態管理 | 現在の運行状態を管理 |
| 記録シート | 入力された日報記録を保存 |
| 日報_拠点別 | 拠点ごとの日報を出力 |
| 日報_店舗別サマリー | 店舗・コースごとの集計を出力 |
| 申し送り一覧 | 備考・申し送りを一覧化 |
| PDF_作成用 | PDF出力用の一時シート |
最初のプロトタイプでは、記録は1つの記録シートに集約する想定でした。
入力された内容を1つの記録シートにためて、そこから日報_拠点別や日報_店舗別サマリーを作る形です。
この時点では、まず動くものを作ることを優先していました。
🔄 状態管理:途中再開できる日報にする
GAS Webアプリにしたことで、状態管理を持たせることができました。
日報入力では、常に「今どこまで進んでいるか」を持つ必要があります。
業務開始前
運行中
拠点に到着済み
休憩中
業務終了
この状態を持っていないと、画面を閉じたときや休憩したときに、どこから再開すればいいか分からなくなります。
状態管理シートでは、主に次のような情報を持たせました。
| 項目 | 役割 |
|---|---|
| 運行ID | 1日の運行を識別する |
| 店舗ID | 対象店舗を識別する |
| 店舗名 | 画面表示用の店舗名 |
| 担当者ID | 担当者を識別する |
| 担当者名 | 画面表示用の担当者名 |
| コースID | 当日のコースを識別する |
| コース名 | 画面表示用のコース名 |
| 現在状態 | 到着待ち、出発待ち、休憩中などを管理する |
| 現在の拠点順 | コース内でどこまで進んだかを管理する |
| 現在の拠点名 | 現在表示する販売場所名 |
| 到着時刻 | 現在拠点の到着時刻 |
| 休憩開始時刻 | 休憩中から復帰するために使用する |
| 最終更新日時 | 状態が最後に更新された時刻 |
🔧 状態管理を取得する処理のコードを見る
function getCurrentState(storeId) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('状態管理');
const values = sheet.getDataRange().getValues();
const header = values.shift();
const storeIdIndex = header.indexOf('店舗ID');
const row = values.find(r => String(r[storeIdIndex]) === String(storeId));
if (!row) return null;
return {
operationId: row[header.indexOf('運行ID')],
status: row[header.indexOf('現在状態')],
staffName: row[header.indexOf('担当者')],
storeName: row[header.indexOf('店舗')],
courseName: row[header.indexOf('コース')],
currentStopOrder: row[header.indexOf('現在の拠点順')],
currentStopName: row[header.indexOf('現在の拠点名')],
arrivalTime: row[header.indexOf('到着時刻')],
breakStartTime: row[header.indexOf('休憩開始時刻')]
};
}
状態管理があることで、画面を閉じても途中から再開できます。
休憩中なら休憩中画面に戻せます。
到着済みなら、客数入力と出発ボタンを表示できます。
この時点では、入力画面、状態管理、自動日報更新まで入ったので、かなり完成に近いと感じていました。
でも、実際に使ってもらうと問題が出ました。
⚠️ 管理担当2人に使ってもらって見えた問題
最初のプロトタイプが一通り動くようになったので、管理担当2人に同時に共有して使ってもらいました。
そこで問題が出ました。
画面の切り替わりが遅く、ボタンを押したあとに次の画面へ進んだのか分かりにくい場面がありました。
その結果、出発ボタンを押したつもりなのに画面が変わらず、何度も押してしまうことがありました。
これは、単に読み込みが遅いというだけではありません。
現場入力では、画面がすぐに反応しないと、
押せていないのでは?
もう一度押した方がいいのでは?
と感じてしまいます。
実際に、読み込みを待っている間に誤入力が起きる可能性がありました。
この時点で、次のような意見が出ました。
2人でこれなら、数台同時に使ったら厳しいのではないか。
この指摘はかなり大きかったです。
実際に使ってもらったことで見えた問題と、それに対する対応を整理すると次のようになります。
| 実際に使って見えたこと | 問題 | 対応 |
|---|---|---|
| 画面切り替わりが遅い | 押せたか分からず再度押してしまう | 保存中はボタンを無効化 |
| 2人同時利用でも重さが見えた | 台数が増えるとさらに厳しい | 現場入力処理を軽量化 |
| 入力のたびに日報更新すると重い | 画面遷移が遅れる | 日報更新・PDF出力を管理者メニューへ分離 |
🚚 100台近い移動スーパー運用には厳しい
移動販売車は、現状でも50台ほど。
今後100台規模での展開も見据える必要があります。
2人で同時に使っただけでも画面遷移の遅れや誤操作の可能性が見えたのであれば、全車両が同じ仕組みを使ったときに問題が大きくなる可能性があります。
特に、移動販売では同じような時間帯に複数店舗が一斉に業務開始、到着、出発、休憩、業務終了を記録する可能性があります。
もし全店舗の処理が1か所に集中すると、次のような問題が起きます。
保存処理が重なる
画面遷移が遅くなる
二重押しが増える
ログの書き込みが集中する
日報更新が重くなる
PDF出力に時間がかかる
最初のプロトタイプは、機能としては動いていました。
でも、実際の運用規模を考えると、このままでは厳しいと感じました。
ここで、考え方を変えました。
🧩 考え方を変えた:自動化より処理速度
最初は、自動で日報まで作ることを重視していました。
入力すれば日報まで完成する。
管理者の手作業が減る。
一見すると、これが一番便利です。
でも、現場入力時に重い処理が走ると、画面遷移が遅れます。
画面遷移が遅れると、誤入力につながります。
現場担当者にとって大事なのは、入力した瞬間に完璧な日報が完成することではありません。
販売の合間に、軽く、確実に、迷わず記録できることです。
そこで、自動化よりも処理速度を優先することにしました。
今回の設計変更は、
「もっと自動化する」方向ではなく、「現場入力時に自動でやりすぎない」方向への見直し でした。
🧠 技術的に工夫したところ
今回の実装で特に意識したのは、次の4点です。
| 工夫した点 | 内容 |
|---|---|
| 状態管理 | 現在の拠点、到着済み、休憩中などをスプレッドシートで保持 |
| 責務分離 | 現場入力時はログ保存と状態更新だけにする |
| 店舗別ログ | 1つの記録シートに集中させず、店舗IDごとにログを分ける |
| 二重送信対策 | 保存中のボタン無効化とLockServiceで誤入力・同時保存に対応 |
この4点によって、現場入力の操作感をできるだけ軽くしつつ、あとから日報やPDFとして見返せる形にしました。
🔧 軽量化の実装:現場入力はログ保存と状態更新だけにする
一番大きく変えたのは、現場入力時に行う処理です。
LINE Bot + Make版では、Make側で状態管理シートを参照しながら入力内容に応じて処理を分岐していました。
GAS Webアプリ版では、この考え方を引き継ぎつつ、画面制御と状態更新をGAS側で扱う形に変えました。
さらに、最初のGASプロトタイプでは、入力のたびに日報更新まで行う想定でした。
入力
↓
ログ保存
↓
状態更新
↓
日報更新
↓
シート整形
↓
画面更新
この形は、入力した内容がすぐ日報に反映されるので、一見便利です。
ただし、現場入力のたびに日報更新やシート整形まで行うと、画面遷移が重くなります。
そこで、変更後は、現場入力時に日報更新やPDF出力を呼ばないようにしました。
🔧 変更後:現場入力時はログ保存と状態更新だけにする
function recordDeparture(customerCount, remark) {
const state = getActiveOperationState();
const now = new Date();
const record = {
operationId: state.operationId,
recordDateTime: now,
processType: '出発',
storeId: state.storeId,
storeName: state.storeName,
courseId: state.courseId,
courseName: state.courseName,
stopOrder: state.currentStopOrder,
stopName: state.currentStopName,
arrivalTime: state.arrivalTime,
departureTime: now,
customerCount: customerCount,
remark: remark
};
appendLogRow(record);
updateStateToNextStop(state);
// 日報更新やPDF出力はここでは呼ばない
return getCurrentScreenData(state.storeId);
}
変更後の recordDeparture() では、現場入力時に行う処理を次の3つに絞りました。
appendLogRow(record)
updateStateToNextStop(state)
getCurrentScreenData(state.storeId)
ボタン押下時に同期的に実行する処理と、管理者が後から実行する後処理を分けました。
| 処理 | 実行タイミング |
|---|---|
| ログ保存 | 現場入力時 |
| 状態管理の更新 | 現場入力時 |
| 次画面データの取得 | 現場入力時 |
| 日報更新 | 管理者メニューから実行 |
| シート整形 | 管理者メニューから実行 |
| PDF出力 | 管理者メニューから実行 |
これにより、現場担当者がボタンを押したときの処理を軽くし、画面遷移を早くすることを優先しました。
🗃 店舗別ログ:1つの記録シートから店舗別ログへ変更
以前の考え方では、すべての店舗の記録を1つの記録シートに集約する想定でした。
ただ、60台以上の移動販売車が使うことを考えると、1つの記録シートに書き込みが集中します。
そこで、店舗IDごとにログシートを作るようにしました。
記録_7001
記録_7002
記録_7003
記録_7004
店舗名ではなく店舗IDを使ったのは、店舗名変更や同名店舗の影響を避けるためです。
店舗名は表示用、店舗IDは記録・集計・売上データ突合用として扱います。
🔧 店舗別ログシート名を作るコードを見る
function getStoreLogSheetName(storeId) {
return `記録_${String(storeId).trim()}`;
}
🔧 店舗別ログシートを取得・作成するコードを見る
function getLogSheetByStoreId(storeId) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheetName = getStoreLogSheetName(storeId);
let sheet = ss.getSheetByName(sheetName);
if (!sheet) {
sheet = ss.insertSheet(sheetName);
sheet.appendRow([
'運行ID',
'記録日時',
'運行日',
'曜日',
'処理内容',
'担当者ID',
'担当者',
'店舗ID',
'店舗',
'コースID',
'コース',
'拠点順',
'拠点名',
'到着時刻',
'出発時刻',
'滞在時間',
'客数',
'備考',
'データ区分',
'有効'
]);
}
return sheet;
}
店舗別ログに分けたことで、1つの記録シートに書き込みが集中するのを避け、店舗ごとの記録も確認しやすくなりました。
🔒 誤入力・二重押しを防ぐ仕組みを追加する
2人に同時に使ってもらったとき、画面が切り替わらず、出発ボタンを何度も押してしまう場面がありました。
そのため、同時入力や二重押しへの対策は、画面側とGAS側の両方で行いました。
| 対策する場所 | 内容 |
|---|---|
| 画面側 | 保存中はボタンを無効化して、連打や二重送信を防ぐ |
| GAS側 | LockServiceで同時保存処理を順番に実行する |
| 設計側 | エリア分割と店舗別ログで、処理の集中を避ける |
画面側では、保存中にボタンを無効化します。
🔧 画面側:保存中はボタンを無効化するコードを見る
function setSaving(isSaving) {
const buttons = document.querySelectorAll('button');
buttons.forEach(button => {
button.disabled = isSaving;
});
const status = document.getElementById('savingStatus');
if (status) {
status.textContent = isSaving ? '保存中です...' : '';
}
}
GAS側では、LockService を使って、保存処理が同時に走った場合でも順番に処理されるようにしました。
🔧 GAS側:LockServiceで同時書き込みを制御するコードを見る
function appendLogRow(record) {
const lock = LockService.getDocumentLock();
let locked = false;
try {
lock.waitLock(10000);
locked = true;
const sheet = getLogSheetByStoreId(record.storeId);
sheet.appendRow([
record.operationId,
record.recordDateTime,
record.operationDate,
record.weekday,
record.processType,
record.staffId,
record.staffName,
record.storeId,
record.storeName,
record.courseId,
record.courseName,
record.stopOrder,
record.stopName,
record.arrivalTime,
record.departureTime,
record.stayMinutes,
record.customerCount,
record.remark,
'本番',
true
]);
} finally {
if (locked) {
lock.releaseLock();
}
}
}
画面側では連打を防ぎ、GAS側では同時書き込みを制御する。
この2段階で、二重送信やログの衝突を防ぐ設計にしました。
🗂 エリアごとの運用に切り替える
さらに、全店舗を1つのアプリと1つのスプレッドシートで扱うのではなく、エリアごとに分けて運用する方針にしました。
現状でも移動販売車は60台以上あります。
今後は100台規模も見据える必要があります。
この規模を1つのアプリ、1つのシートで処理するのは重くなります。
そこで、最初からエリア単位で分けて横展開できる構成にしました。
Aエリア:10〜20店舗程度
Bエリア:10〜20店舗程度
Cエリア:10〜20店舗程度
...
エリアごとに分ければ、1つのアプリやスプレッドシートに処理が集中しにくくなります。
店舗別ログと組み合わせることで、さらに処理の集中を避けられます。
ここで、最初に目指していた「入力すれば自動で日報まで完成する」形から、考え方が変わりました。
現場入力では軽く保存する。
日報更新やPDF出力は管理者が後から実行する。
この形にしたことで、実際の運用規模に合わせやすくなりました。
🧑💼 管理者メニュー:日報更新・PDF出力は後から実行する
重い処理は、現場入力時には実行しません。
日報更新、シート整形、PDF出力は、スプレッドシートを開いた管理者がメニューから実行できるようにしました。
🔧 管理者メニューを作成するコードを見る
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('移動スーパー業務アプリ')
.addItem('日報を更新', 'refreshDailyReports')
.addItem('全シートを整形', 'formatAllSheets')
.addItem('店舗別記録シートを準備', 'prepareStoreLogSheets')
.addSeparator()
.addItem('本日分PDFを作成(店舗別)', 'createTodayStorePdfs')
.addItem('日付指定PDFを作成(店舗別)', 'createStorePdfsByPromptDate')
.addToUi();
}
これにより、現場入力時の処理は軽くしつつ、管理者は必要なタイミングで日報やPDFを作成できます。
📄 日報出力:まとめシートと店舗別PDFに分けた理由
日報出力は、1つの形式にまとめるのではなく、目的に応じて分けました。
大きく分けると、日報には2つの役割があります。
1. 管理者・企画側が、全体を集計して分析するための日報
2. 現場担当者・店舗が、その日の運行結果を見返すための日報
そのため、出力を次のように分けました。
| 出力 | 見る人 | 目的 |
|---|---|---|
| 日報_拠点別 | 管理者・企画側 | 拠点ごとの記録を一覧で確認する |
| 日報_店舗別サマリー | 管理者・企画側 | 店舗・コース単位で実績を集計する |
| 申し送り一覧 | 管理者・現場担当者 | 備考や申し送りを確認する |
| 店舗別PDF | 現場担当者・店舗 | 店舗ごとの日報として見返す・共有する |
まとめシートは、将来的なデータ活用を考えています。
今後、販売Rawデータと紐付けることで、拠点ごとの売上、客数、販売商品、曜日別実績などを見られるようにしたいと考えています。
そのため、管理者向けのまとめシートでは、後から売上データや商品データと紐付けやすいように、店舗ID、コースID、拠点順、拠点名、日付、曜日などを残す設計にしました。
一方で、店舗別PDFは、現場担当者や店舗が見るためのものです。
現場や店舗で見返すときには、内部IDや集計用の細かい列が多すぎると読みにくくなります。
必要なのは、その日の運行結果、客数、スキップ、備考、申し送りが分かることです。
同じ日報データでも、管理者が分析するための形と、現場が確認するための形は違います。
そこで、まとめシートと店舗別PDFを分けることにしました。
📄 PDF出力:見返しやすい店舗別日報にする
店舗別PDFは、現場担当者や店舗が見返すための帳票です。
そのため、まとめシートとは分けて、店舗ごとに見やすい形で出力するようにしました。
🔧 PDF出力処理のコードを見る
function exportSheetToPdf(sheet, fileName, folder) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheetId = sheet.getSheetId();
const url =
`https://docs.google.com/spreadsheets/d/${ss.getId()}/export` +
`?format=pdf` +
`&gid=${sheetId}` +
`&size=A4` +
`&portrait=true` +
`&fitw=true` +
`&sheetnames=false` +
`&printtitle=false` +
`&pagenumbers=true` +
`&gridlines=false` +
`&fzr=false`;
const token = ScriptApp.getOAuthToken();
const response = UrlFetchApp.fetch(url, {
headers: {
Authorization: `Bearer ${token}`
}
});
const blob = response.getBlob().setName(fileName);
folder.createFile(blob);
}
PDF出力は時間がかかる処理です。
そのため、現場入力時には実行せず、管理者メニューから実行するようにしました。
🧾 コード変更の整理
今回のコード変更を整理すると、次のようになります。
| 変更箇所 | 変更前 | 変更後 |
|---|---|---|
| 入力時処理 | 入力のたびに日報更新まで行う想定 | ログ保存と状態更新だけにした |
| 記録保存先 | 1つの記録シート | 店舗別ログシート |
| 日報更新 | 自動・都度更新寄り | 管理者メニューから手動実行 |
| PDF出力 | 入力処理に近い位置で考えていた | 管理者メニューから店舗別に出力 |
| 二重押し対策 | 画面反応待ちで誤操作の可能性 | 保存中はボタン無効化 |
| 同時保存対策 | 書き込み衝突の可能性 | LockServiceで順番に処理 |
| 運用単位 | 全体を1つで扱う想定 | エリアごとに分けて横展開 |
見た目の操作画面は大きく変えず、裏側の処理を分けることで軽量化しました。
💡 フィードバックから設計が変わった
今回の制作で大きかったのは、作るたびに次の課題が見えたことです。
LINE Bot + Make
↓
チームから継続運用・料金面の指摘
↓
Googleフォーム案を検討
↓
一方通行の入力では足りないと判断
↓
GAS Webアプリへ作り直し
↓
自動日報作成プロトタイプ
↓
同時利用で処理の重さが見える
↓
ログ保存中心の軽量設計へ変更
LINE Botは、現場担当者が感覚的に使える入口としては良い方法でした。
ただ、継続運用や状態管理まで考えると、別の形が必要でした。
GAS Webアプリも、最初は日報まで自動で作る形を目指しました。
でも、実際に使ってもらうと、現場入力時に重い処理を走らせる危険が見えました。
作る前には見えていなかったことが、作って、見てもらって、使ってもらうことで見えてきました。
その結果、日報Botは単なる入力フォームではなく、現場入力と管理者処理を分けた業務アプリに近づきました。
🚀 今後の展望:売上Rawデータ・商品データ・コース編成へ
今回作ったものは、日報入力と記録の仕組みです。
次にやりたいのは、実際の販売Rawデータや商品別販売データとつなげることです。
移動スーパーでは、拠点ごとの客数だけでなく、売上や販売商品も重要です。
今後は、次のようなデータを見られるようにしたいです。
拠点別売上
拠点別客数
拠点別の商品実績
曜日別実績
コース別売上
販売場所ごとの利用状況
今後は、日報側で持っている次の情報をキーにして、販売Rawデータと紐付ける想定です。
| 日報側の情報 | 紐付けに使う目的 |
|---|---|
| 店舗ID | 店舗別売上データとの突合 |
| 運行日 | 日別販売実績との突合 |
| コースID | コース単位の実績確認 |
| 拠点順・拠点名 | 販売場所単位の傾向確認 |
| 到着時刻・出発時刻 | 販売時間帯との照合 |
| 客数 | 客単価や利用状況の確認 |
これらが見えるようになると、単なる日報入力ではなく、コース編成にも使えるようになります。
たとえば、
利用が少ない拠点を見直す
売上が高い拠点の滞在時間を調整する
売れている商品を拠点ごとに見る
曜日ごとの偏りを見る
訪問順を検討する
新規拠点追加の判断材料にする
といった活用ができます。
今回のプロトタイプは、日報入力で終わるものではなく、将来的には拠点別売上や商品実績を見ながら、コース編成や販売場所の見直しにつなげるための土台として考えています。
✅ 今回のプロトタイプでできたこと
今回できたことを整理します。
| 分類 | できたこと |
|---|---|
| 入力 | スマホから店舗・担当者・コースを選んで記録 |
| 記録 | 店舗別ログに保存 |
| 状態管理 | 途中再開、休憩、担当者変更に対応 |
| 修正 | 直前記録修正に対応 |
| 申し送り | 先週同曜日の備考表示、当日の備考保存 |
| 軽量化 | 現場入力時はログ保存と状態更新だけにした |
| 同時入力対策 | ボタン無効化とLockServiceを入れた |
| 管理 | 日報更新、シート整形 |
| 出力 | 店舗別PDF出力 |
| 運用設計 | エリア単位で横展開する構成 |
本番運用に入れるには、実店舗マスタへの差し替え、権限管理、PDF保存先の整理、月次アーカイブなどが必要です。
ただ、最終制作プロトタイプの第一歩として、日報入力から帳票出力までの基本的な流れは確認できました。
🏁 まとめ:作ってみたから、設計を変える必要が見えた
今回の制作は、LINE Bot + Makeから始まりました。
その後、チームからの指摘を受けて、Googleフォーム案も検討し、最終的にはGAS Webアプリとして作り直しました。
最初のGAS版では、入力すれば日報まで自動で完成する形を目指しました。
しかし、2人同時に使ってもらうと、画面遷移の遅れが誤操作につながることが分かりました。
現状60台以上、将来100台規模の移動販売車で使うなら、現場入力時に重い処理を走らせる設計は危険です。
そこで、現場入力は店舗別ログへの保存と状態管理の更新だけに絞り、日報更新やPDF出力は管理者メニューに分けました。
今回の学びは、単に「もっと自動化すればよい」ではありませんでした。
自動化しすぎないことも、現場で使うための設計だった。
次は、日報データを販売Rawデータや商品データとつなげ、販売場所ごとの商品傾向や売上を見ながら、持ち出し数、販売予測、コース見直しに使える形へ発展させたいです。







