kintone
投稿日:

飲食チェーン12店舗の日報をkintoneで一元管理した話 ― 売上・食材ロス・クレームの見える化

皆さんこんにちは。kintoneアプリエンジニアのtomiokaです。

先日、12店舗を展開する飲食チェーンのお客様から「各店舗の日報がバラバラのExcelで上がってきて、本部での集計に毎週半日かかっている」と相談がありました。

店長12名、パート・アルバイトを含めると約180名が在籍する規模の飲食チェーンです。売上や来客数だけでなく、食材ロスやクレームの記録も店舗ごとに管理方法が異なっていて、本部のエリアマネージャーが毎週月曜日にExcelを12ファイル開いて手作業で集計しているという状況でした。

この記事の想定読者

この記事は、以下のような方を想定しています。

  • 5〜30店舗規模の飲食チェーンで、本部管理やエリアマネジメントを担当されている方
  • 店舗日報の集計に時間がかかっていると感じているマネージャーの方
  • 食材ロスやクレームの傾向を可視化したいが、やり方がわからない経営企画の方

「うちも似たような状況だな」と感じた方は、参考にしていただけると思います。

Excelベースの店舗日報が抱える問題

お客様にヒアリングしてみると、問題は大きく3つありました。

1つ目は、フォーマットの不統一です。12店舗それぞれの店長が独自にExcelをカスタマイズしていて、列の並びも入力ルールもバラバラでした。ある店舗は売上を税込で入力、別の店舗は税抜で入力、、、、こうなると集計のたびに確認作業が発生します。

2つ目は、リアルタイム性のなさです。日報はメールにExcelを添付して本部に送る運用だったのですが、送り忘れや送信の遅延が日常的に起きていました。月曜朝の時点で全店舗分の日報が揃っていることはほとんどなかったそうです。

3つ目は、クレーム情報の埋没です。日報の備考欄にクレーム内容を書いている店舗もあれば、まったく記録していない店舗もありました。これではクレームの傾向分析なんてとてもできる状態ではなかったんです。

アプリ構成

今回の構成は3つのアプリで成り立っています。それぞれの役割を整理すると以下のようになります。

アプリ名役割主な用途
店舗マスタ店舗情報の一元管理店舗名・住所・店長名・席数などの基本情報を管理
店舗日報日次の営業データ記録売上・来客数・食材仕入/ロス・天候・備考を毎日入力
クレーム管理クレーム情報の個別管理発生日時・内容・対応状況・再発防止策を1件ずつ記録

店舗日報アプリから店舗マスタをルックアップで参照する形にしました。クレーム管理アプリは店舗日報とは独立させています。クレームは1件1レコードで詳細を記録したいので、日報のサブテーブルに入れるのではなく別アプリにしたんです。日報側には「クレーム有無」のラジオボタンだけ置いて、「あり」の場合にクレーム管理アプリへ別途登録する運用にしています。

店舗日報アプリのフィールド構成

店舗日報アプリのフィールドは以下の通りです。

フィールド名フィールドタイプ備考
報告日日付必須。当日の日付を初期値に設定
店舗名ルックアップ店舗マスタから取得
エリア文字列(1行)ルックアップのコピー元フィールド
売上金額(税抜)数値必須。単位:円
来客数数値必須
客単価計算売上金額 ÷ 来客数 で自動算出
食材仕入金額数値当日の食材仕入額
食材ロス金額数値廃棄した食材の金額
食材ロス率計算食材ロス金額 ÷ 食材仕入金額 × 100 で自動算出
天候ドロップダウン晴れ / 曇り / 雨 / 雪
クレーム有無ラジオボタンなし / あり
備考文字列(複数行)自由記入欄

ポイントは、売上金額を「税抜」で統一したことです。これだけで集計時のブレがなくなります。客単価と食材ロス率は計算フィールドで自動算出するので、店舗側の入力の手間もかかりません。

食材ロス率の閾値アラート表示

今回のカスタマイズで一番喜ばれたのが、食材ロス率が基準値を超えたときにレコード画面の上部にアラートバナーを表示する機能です。

お客様から「ロス率が高い日をつい見落としてしまう」という声があったので、レコードの詳細画面や編集画面を開いた瞬間に目に入るようにしました。閾値はお客様と相談して5%に設定しています。さらに、編集画面や追加画面では食材ロス金額・仕入金額を変更するたびにリアルタイムで再計算されます。

このコードは詳細画面・編集画面・追加画面で動作します。

(function() {
  'use strict';

  // 食材ロス率の閾値(%)
  var LOSS_RATE_THRESHOLD = 5;

  // アラートバナーを表示する関数
  function showAlert(rate) {
    var existing = document.getElementById('loss-rate-alert');
    if (existing) {
      existing.parentNode.removeChild(existing);
    }

    var el = document.createElement('div');
    el.id = 'loss-rate-alert';
    el.style.cssText =
      'background:#fee2e2;border:2px solid #ef4444;color:#b91c1c;' +
      'padding:12px 16px;margin:8px 0;border-radius:6px;' +
      'font-weight:bold;font-size:14px;';
    el.textContent =
      '⚠ 食材ロス率が ' + rate.toFixed(1) +
      '% です(基準値:' + LOSS_RATE_THRESHOLD +
      '%以下)。店長確認が必要です。';

    var header = kintone.app.record.getHeaderMenuSpaceElement();
    if (header) {
      header.appendChild(el);
    }
  }

  // アラートを消す関数
  function removeAlert() {
    var existing = document.getElementById('loss-rate-alert');
    if (existing) {
      existing.parentNode.removeChild(existing);
    }
  }

  // 詳細・編集・追加画面の表示時にチェック
  kintone.events.on([
    'app.record.detail.show',
    'app.record.edit.show',
    'app.record.create.show'
  ], function(event) {
    var record = event.record;
    var lossCost = Number(record['食材ロス金額'].value) || 0;
    var purchaseCost = Number(record['食材仕入金額'].value) || 0;

    if (purchaseCost > 0) {
      var rate = (lossCost / purchaseCost) * 100;
      if (rate > LOSS_RATE_THRESHOLD) {
        showAlert(rate);
      } else {
        removeAlert();
      }
    }
    return event;
  });

  // 編集・追加画面でフィールド変更時にリアルタイム再計算
  kintone.events.on([
    'app.record.edit.change.食材ロス金額',
    'app.record.edit.change.食材仕入金額',
    'app.record.create.change.食材ロス金額',
    'app.record.create.change.食材仕入金額'
  ], function(event) {
    var record = event.record;
    var lossCost = Number(record['食材ロス金額'].value) || 0;
    var purchaseCost = Number(record['食材仕入金額'].value) || 0;

    if (purchaseCost > 0) {
      var rate = (lossCost / purchaseCost) * 100;
      if (rate > LOSS_RATE_THRESHOLD) {
        showAlert(rate);
      } else {
        removeAlert();
      }
    } else {
      removeAlert();
    }
    return event;
  });
})();

getHeaderMenuSpaceElement() でレコード上部のメニュースペースを取得し、そこにアラートバナーを差し込んでいます。閾値の 5 は定数として切り出しているので、業態に合わせて調整しやすい作りです。

ハマりどころ

ルックアップのコピー先フィールドが編集不可になる

店舗マスタからルックアップで「エリア」をコピーしているのですが、コピー先のフィールドはkintoneの仕様上、ユーザーが手動で編集できません。最初これに気づかず、、、、「エリアの表示がおかしいんですけど」とお客様から連絡が来て焦りました。コピー先の値が間違っている場合はマスタ側のデータを直すか、ルックアップの再取得が必要です。

計算フィールドの値がchangeイベントで取得できるタイミング

食材ロス率は計算フィールドで算出していますが、JavaScriptの change イベント内で計算フィールドの値を参照すると、、、、更新前の古い値が返ってくることがあります。今回のコードでは計算フィールドの値を使わずに、元の数値フィールド(食材ロス金額・食材仕入金額)から直接JavaScriptで計算しています。kintoneカスタマイズではほぼ必ずと言っていいほど発生するハマりポイントなので、覚えておくと安心です。

getHeaderMenuSpaceElementの一覧画面と詳細画面の違い

kintone.app.record.getHeaderMenuSpaceElement() はレコード詳細・編集画面専用のAPIです。一覧画面で同じことをしようとすると null が返ります。一覧画面にアラートを出したい場合は kintone.app.getHeaderMenuSpaceElement()(recordなし)を使う必要があります。名前が似ていて混同しやすいんですよね、、、、

導入してどう変わったか

kintoneでの運用を開始して1か月後、お客様からうれしい報告をいただきました。

  • 日報の集計作業が毎週半日からほぼゼロに短縮された。kintoneのグラフ機能で店舗別・エリア別の売上をリアルタイムに確認できるようになった
  • 食材ロス率のアラート表示により、ロス率5%超の日を見逃さなくなった。月間の食材廃棄額が前月比で約15%減少した
  • クレーム管理アプリで対応漏れがなくなり、再発防止策が全店舗で共有されるようになった
  • 12店舗の店長が同じフォーマットで入力するため、入力ルールの個人差がなくなった

特にエリアマネージャーの方が「月曜朝にダッシュボードを見るだけで全店舗の状況がわかるようになった」と言ってくださったのが印象的でした。

まとめ

今回は、飲食チェーン12店舗の店舗日報をkintoneで一元管理した事例を紹介しました。

Excelベースの日報管理は、店舗数が10を超えたあたりから集計コストが一気に膨らみます。フォーマットの統一、リアルタイムの入力、アラートによる異常検知を仕組み化することで、本部の負担は大幅に減らせます。

今回の構築で押さえておきたいポイントは以下の3つです。

  • 売上は「税抜」でフォーマットを統一し、客単価・食材ロス率は計算フィールドで自動算出する
  • 食材ロス率のような重要指標は閾値を設けて、JavaScriptでアラート表示する仕組みを入れる
  • クレーム管理は日報とは別アプリにして、1件1レコードで詳細を追跡できるようにする

kintoneの標準機能とちょっとしたカスタマイズの組み合わせで、現場の運用はかなり変わります。「うちの店舗管理でも使えそうだな」と思った方は、ぜひ検討してみてください。

株式会社ファストコーディングでは、kintoneのカスタマイズや業務アプリの構築を承っています。お問い合わせフォームからお気軽にご連絡ください。