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

# トイレットペーパーの在庫、どれくらいありますか？
少し前、「大沢たかお祭り」でも話題になっていましたが、
買い物して帰ったら、家になかったはずのマヨが2本買ってた経験、ありませんか？

https://x.com/kiyu_yuno/status/1919906208162746738

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

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

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

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

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

# こんな仕組みで作った!

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

![スクリーンショット 2025-05-20 20.22.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2959760/60dc2116-3e11-46c3-9cc7-f1faf6c10aea.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2959760/d975fd26-6cb4-4026-9246-727f9dad11c7.png)
　

![スクリーンショット 2025-05-20 21.06.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2959760/a7dabbf5-0049-4096-942d-62b1376fb90e.png)

**残り1個！メールが届きました！📩**
![スクリーンショット 2025-05-20 21.06.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2959760/82b1ea67-e43b-4879-92c0-72cbbefe47c3.png)

## アイテム追加もできます

「芳香剤」を追加してみたら…
![スクリーンショット 2025-05-20 21.10.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2959760/1f0e4def-b4ce-48b5-857f-7ee349bdc6ef.png)

**一瞬で追加！🎉** 


![スクリーンショット 2025-05-20 21.10.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2959760/f9e41338-06ce-47b1-80b5-c44e93c7b9e9.png)

同じ名前で登録しようとすると…
![スクリーンショット 2025-05-20 21.09.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2959760/13931579-2fe7-41e1-8d3f-22047acf6c82.png)

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


# スマホにも設置 （￣+ー￣）
Chromeで開いた状態からショートカットに追加し、
**好きなアイコンと色** で保存できます！
![IMG_5893.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2959760/332a971c-070c-4635-b7f5-8190b36cb404.png)

ホーム画面に追加すると、まるでアプリみたいに使える！
![IMG_5894.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2959760/0fd13b67-28b1-4c50-a899-743e6de061a7.png)

起動すると、PCと同じようにちゃんと反映されます。
![IMG_5895.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2959760/f8a12226-0f03-468f-b601-da9749612533.png)

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



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

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