3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

買いすぎ・買い忘れ・心の乱れ、家の在庫問題が全部GASで解決しました!

Last updated at Posted at 2025-05-20

トイレットペーパーの在庫、どれくらいありますか?

少し前、「大沢たかお祭り」でも話題になっていましたが、
買い物して帰ったら、家になかったはずのマヨが2本買ってた経験、ありませんか?

私、あります。
めちゃくちゃあります。

気づいたら、押し入れに大容量洗剤の詰め替えが5個、
トイレットペーパーはなんと 24ロール × 5パック…

ちゃんと数えたら、来年まで持つやん…ショック!

自分を救うため、自宅在庫管理アプリを作ってみた!

毎回買いすぎて落ち込む自分を救うために、在庫管理アプリを作りました!
しかも、全部Google Apps Script(GAS) で動いてます!

こんな仕組みで作った!

自宅在庫管理アプリはすべてGASで作成しました!!

スクリーンショット 2025-05-20 20.22.57.png

  1. GAS + HTML でWebアプリを作成

  2. アイテム一覧と在庫数を表示

  3. 「使った/補充した」操作をすると、スプレッドシートに即反映

  4. 在庫が1個になったらメールで通知

  5. iPhoneのショートカットに登録して、スマホからもすぐアクセス可能!

GASのコード(抜粋)

code.gsの一部です。

function doGet() {
  return HtmlService.createHtmlOutputFromFile('form'); //好きな名前に変更してください
}

// アイテム一覧と在庫を取得
function getItemsWithStock() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Stock');
  const data = sheet.getRange(2, 1, sheet.getLastRow() - 1, 2).getValues(); // アイテム名+在庫
  return data.map(row => ({ name: row[0], stock: row[1] }));
}

// 在庫を1つ減らす
function recordUse(itemName) {
  return updateStock(itemName, -1);
}

// 在庫を1つ補充する
function recordSupply(itemName) {
  return updateStock(itemName, 1);
}

// 在庫更新ロジック
function updateStock(itemName, diff) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Stock');
  const data = sheet.getDataRange().getValues();

  for (let i = 1; i < data.length; i++) {
    if (data[i][0] === itemName) {
      const currentStock = data[i][1];
      const newStock = Math.max(0, currentStock + diff);
      sheet.getRange(i + 1, 2).setValue(newStock);
      sheet.getRange(i + 1, 3).setValue(new Date());

      if (newStock === 1) {
        sendNotification(itemName);
      }

      return `「${itemName}」の在庫数:${currentStock} → ${newStock}`;
    }
  }
  return "エラー:該当アイテムが見つかりません";
}

function getItemsWithStock() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Stock');
  const data = sheet.getRange(2, 1, sheet.getLastRow() - 1, 3).getValues(); // A列~C列
  return data.map(row => ({
    name: row[0],
    stock: row[1],
    updated: row[2] ? Utilities.formatDate(new Date(row[2]), Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm") : "-"
  }));
}

function addNewItem(itemName) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Stock');
  const data = sheet.getRange(2, 1, sheet.getLastRow() - 1, 1).getValues().flat();
  if (data.includes(itemName)) {
    return "すでに存在するアイテムです。";
  }

  const now = new Date();
  sheet.appendRow([itemName, 1, now]);
  return "アイテムを追加しました!";
}


// 在庫が1になったらメール通知
function sendNotification(itemName) {
  const email = "xxx@test.com"; // ←自分のアドレス
  const subject = `在庫警告: ${itemName} の在庫が1になりました`;
  const body = `${itemName} の在庫が残り1個です。補充しましょう!!`;
  MailApp.sendEmail(email, subject, body);
}

form.html では、以下のように操作できます。

  1. 在庫数の表示

  2. アイテムの追加

  3. 「使う」「補充」の操作

  4. 在庫一覧の更新

<!DOCTYPE html>
<html>
  <body>
    <h2>在庫管理アプリ</h2>

    <label for="itemSelect">アイテム選択:</label>
    <select id="itemSelect"></select>
    <div id="stockDisplay">在庫数: -</div>

    <button onclick="useItem()">1個使う</button>
    <button onclick="supplyItem()">1個補充</button>

<h3>🆕 アイテムを追加</h3>
<input type="text" id="newItemName" placeholder="新しいアイテム名を入力" style="width:100%; padding:10px;">
<button onclick="addItem()">アイテムを追加</button>

    <p id="result"></p>
    <h3>📋 在庫一覧</h3>
    <div class="table-wrapper">
      <table id="stockTable">
        <thead>
          <tr>
            <th>アイテム名</th>
            <th>在庫数</th>
            <th>最終更新</th>
          </tr>
        </thead>
        <tbody></tbody>
      </table>
    </div>

    <script> // ここで、htmlの操作をGAS側へ送信する
      let items = [];

      // 初期化
      google.script.run.withSuccessHandler(function(result) {
        items = result;
        const select = document.getElementById("itemSelect");
        select.innerHTML = "";
        result.forEach(function(item) {
          const option = document.createElement("option");
          option.value = item.name;
          option.text = item.name;
          select.appendChild(option);
        });
        updateStockDisplay();
        loadTable(result);
      }).getItemsWithStock();

      document.getElementById("itemSelect").addEventListener("change", updateStockDisplay);

      function updateStockDisplay() {
        const selected = document.getElementById("itemSelect").value;
        const item = items.find(i => i.name === selected);
        document.getElementById("stockDisplay").innerText =
          item ? `在庫数: ${item.stock}` : "在庫数: -";
      }

      function useItem() {
        const selected = document.getElementById("itemSelect").value;
        google.script.run.withSuccessHandler(function(msg) {
          document.getElementById("result").innerText = msg;
          refresh(); // 都度リロード
        }).recordUse(selected);
      }

      function supplyItem() {
        const selected = document.getElementById("itemSelect").value;
        google.script.run.withSuccessHandler(function(msg) {
          document.getElementById("result").innerText = msg;
          refresh();  // 都度リロード
        }).recordSupply(selected);
      }

      function refresh() {
        google.script.run.withSuccessHandler(function(result) {
          items = result;
          updateStockDisplay();
          loadTable(result);
        }).getItemsWithStock();
      }

      function loadTable(data) {
        const tbody = document.getElementById("stockTable").querySelector("tbody");
        tbody.innerHTML = "";
        data.forEach(function(item) {
          const row = document.createElement("tr");
          row.innerHTML = `
            <td>${item.name}</td>
            <td>${item.stock}</td>
            <td>${item.updated}</td>
          `;
          tbody.appendChild(row);
        });
      }

      function addItem() {
  const itemName = document.getElementById("newItemName").value.trim();
  if (!itemName) {
    alert("アイテム名を入力してください!");
    return;
  }

  google.script.run.withSuccessHandler(function(msg) {
    alert(msg);
    document.getElementById("newItemName").value = "";
    refresh(); // 更新
  }).addNewItem(itemName);
}
    </script>
  </body>
</html>

工夫した部分

  1. GASのdoGet()でWebアプリ化し、リンクでアクセス可能に
  2. データはスプレッドシートをバックエンドとして利用
  3. 在庫が1になったらメールでアラート(LINE通知も今後やりたい…!)
  4. モバイルでも使いやすいよう、ショートカット経由で操作可能

⚠ 注意点

・htmlのheadとcssの部分は割愛しました。
・スプレッドシートのシート名は "Stock" にしてください!

実際に使ってみた!

在庫を減らすと…

洗剤の在庫を1つ減らすと…
スクリーンショット 2025-05-20 21.05.58.png
 

スクリーンショット 2025-05-20 21.06.32.png

残り1個!メールが届きました!📩
スクリーンショット 2025-05-20 21.06.42.png

アイテム追加もできます

「芳香剤」を追加してみたら…
スクリーンショット 2025-05-20 21.10.10.png

一瞬で追加!🎉

スクリーンショット 2025-05-20 21.10.16.png

同じ名前で登録しようとすると…
スクリーンショット 2025-05-20 21.09.28.png

「すでに存在するアイテムです」 と表示され、ダブり防止にもなります!

スマホにも設置 ( ̄+ー ̄)

Chromeで開いた状態からショートカットに追加し、
好きなアイコンと色 で保存できます!
IMG_5893.PNG

ホーム画面に追加すると、まるでアプリみたいに使える!
IMG_5894.PNG

起動すると、PCと同じようにちゃんと反映されます。
IMG_5895.PNG

改善したい点・今後のアイデア

  1. スマホ対応(レスポンシブ) ができたら、もっと見やすくて便利
  2. 通知タイミングの改善
     → 通知後に買いに行く人が多いので、一覧表示をもっと見やすくしたい
  3. アイテム削除機能も欲しい(うっかり追加しすぎ防止に)
  4. Amazonリンク付きで 「よく買い忘れるもの」 を表示して、タップで即注文できる仕組みもアリかも!

おわりに

とにかく!! 在庫を「感覚」で管理する時代は終わり!
週末の買い出しに在庫を覚えなくていい日々を目指して、
買いすぎ防止・買い忘れ防止、そして心の平穏 のために!

しばらく使ってみて、また改善できたら更新するかもしれません。
ではー!!

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?