Google Apps Script で家庭内在庫管理アプリを作った

はじめに

食材やトイレットペーパなどの在庫管理がしたい。との要望を受けて、Google Apps Script で家庭内在庫管理アプリを作った。データはGoogleスプレッドシートに保存し、Webページから更新、確認可能。

なお本アプリは勉強もかねて、ほぼChatGPTで作成した。流れとしては、簡単な仕様で実装してもらう→細かいところを修正→最後に仕様をまとめてもらうといった感じ。以下、仕様。

参考にしたサイト様

www2.kobe-u.ac.jp

tonari-it.com

GoogleスプレッドシートエディタWebアプリ 仕様

概要
機能
  • データ表示: スプレッドシート内のデータ(アイテム名と数量、およびグループ名)をWebページ上のテーブルで表示。

  • 数量の更新:

    • 各アイテムの数量を直接入力フィールドから編集可能。
    • 更新ボタンをクリックすることで、スプレッドシート内のデータが更新される。
  • アイテムの追加:
    • 新しいアイテム名と数量を入力フィールドから入力。
    • 既存のグループを選択、または新しいグループ名を入力してアイテムを追加。
    • アイテム追加ボタンをクリックすることで、スプレッドシートに新しい行が追加される。
  • アイテムの削除:
    • 各アイテムの横に配置された削除ボタンをクリックすることでアイテムを削除。
    • 削除操作前に、確認画面が表示される。
レイアウト & デザイン
  • グループ表示:
    • 各グループの名前は、テーブルの見出しとして表示される。
    • 見出しのテキストは太字、20pxのフォントサイズ、下線付きで表示される。
  • レスポンシブデザイン:

実装方法

Google Apps Scriptのセットアップ
  • Googleスプレッドシートを開き、拡張機能 > Apps Script を選択してGoogle Apps Scriptのエディタを開きます。
  • 新しいHTMLファイルを追加し、それをウェブページとして使用します。例えば、Page.htmlという名前でファイルを作成します。
コードの記述

※コードは後述

デプロイ

コード

Code.gs

function doGet() {
  return HtmlService.createHtmlOutputFromFile('Page').setTitle('Spreadsheet Editor').addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

function getSheetData() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  return sheet.getDataRange().getValues();
}

function updateSheetData(data) {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  sheet.clear();
  data.forEach(function(row) {
    sheet.appendRow(row);
  });
}

function addNewItem(group, itemName, quantity) {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  sheet.appendRow([group, itemName, quantity]);
}

function deleteItem(rowIndex) {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  sheet.deleteRow(rowIndex + 1);
}
  • Page.html
<!DOCTYPE html>
<html>
  <head>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            font-size: 20px;
        }

        table {
            width: 100%;
            border-collapse: collapse;
        }


        th, td {
                padding: 1px 1px;
        }

        input[type="text"] { width: 20%; }
        input[type="number"] { width: 20%; }

        @media (max-width: 600px) {
            body {
                font-size: 14px;
            }

            table, th, td {
                padding: 1px 1px;
            }

            button {
                font-size: 0.8em;
            }
        }
    </style>
  </head>
  <body>
    <table id="itemsTable">
        <!-- Items will be displayed dynamically here -->
    </table>
    <button onclick="loadData()">データを再読み込み</button>
    <hr>
    グループ: 
    <select id="groupSelector">
        <!-- Groups will be added dynamically here -->
    </select> 
    または 新規グループ名: <input id="newGroupName" type="text"><br>
    アイテム名: <input id="newItemName" type="text">
    数量: <input id="newItemQuantity" type="number">
    <button onclick="addItem()">アイテム追加</button>
    
    <script>      
        function updateGroupOptions(groups) {
            var groupSelector = document.getElementById("groupSelector");
            groupSelector.innerHTML = "";  // 既存のオプションをクリア
            
            groups.forEach(function(group) {
                var option = document.createElement("option");
                option.value = group;
                option.textContent = group;
                groupSelector.appendChild(option);
            });
        }
        function loadData() {
            google.script.run.withSuccessHandler(function(data) {
                var uniqueGroups = [...new Set(data.map(item => item[0]))];  // グループの一意な一覧を取得
                updateGroupOptions(uniqueGroups);  // グループのオプションを更新
                showData(data);  // テーブルにデータを表示
            }).getSheetData();
        }
        function showData(items) {
            var table = document.getElementById("itemsTable");
            table.innerHTML = "";

            // グループごとにアイテムを整理
            var groupedItems = {};
            items.forEach(function(item) {
                if (!groupedItems[item[0]]) {
                    groupedItems[item[0]] = [];
                }
                groupedItems[item[0]].push(item);
            });

            // グループごとにアイテムをテーブルに追加
            for (var group in groupedItems) {
                var row = table.insertRow(-1);
                var cell1 = row.insertCell(0);
                cell1.innerHTML = group;  // グループ名
                cell1.colSpan = 5;  // グループ名が5列分の幅を持つように設定
                cell1.style.fontWeight = 'bold';  // ボールド体
                cell1.style.fontSize = '20px';   // 20pxのフォントサイズ(あるいは他の希望のサイズに調整)
                cell1.style.textDecoration = 'underline';

                groupedItems[group].forEach(function(item, index) {
                    var subRow = table.insertRow(-1);
                    
                    var cell2 = subRow.insertCell(0);  // アイテム名
                    cell2.innerHTML = item[1];
                    cell2.style.width = '45%';

                    var cell3 = subRow.insertCell(1);  // 数量
                    var input = document.createElement("input");
                    input.style.width = '25%';
                    input.value = item[2];
                    cell3.appendChild(input);
                    cell3.style.width = '25%';

                    var cell4 = subRow.insertCell(2);  // 更新ボタン
                    var updateButton = document.createElement("button");
                    updateButton.innerHTML = "更新";
                    updateButton.onclick = function() {
                        items[index][2] = parseInt(input.value);
                        google.script.run.updateSheetData(items);
                    };
                    cell4.appendChild(updateButton);

                    var cell5 = subRow.insertCell(3);  // 削除ボタン
                    var deleteButton = document.createElement("button");
                    deleteButton.innerHTML = "削除";
                    deleteButton.onclick = function() {
                        if (window.confirm("このアイテムを削除してもよろしいですか?")) {
                            google.script.run.withSuccessHandler(loadData).deleteItem(index);
                        }
                    };
                    cell5.appendChild(deleteButton);
                });
            }
        }

        function addItem() {
            var groupSelector = document.getElementById("groupSelector");
            try {
              var selectedGroup = groupSelector.options[groupSelector.selectedIndex].value;
            }
            catch {
              var selectedGroup = Other;
            }
            var newGroup = document.getElementById("newGroupName").value;
            var groupName = newGroup ? newGroup : selectedGroup;
            
            var itemName = document.getElementById("newItemName").value;
            var itemQuantity = parseInt(document.getElementById("newItemQuantity").value);
            
            google.script.run.withSuccessHandler(loadData).addNewItem(groupName, itemName, itemQuantity);
        }
        // Load data on initial page load
        loadData();
    </script>

</body>
</html>