2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Webアプリ】研修医がGoogleスプレッドシートとGASでDr.コトー診療所の在庫管理アプリを作った話

Last updated at Posted at 2024-10-14

医療現場において低コストでのDX化に挑戦しました。
ご協力頂いた皆様に改めて感謝します。

診療所内のポスター
頂いた反響

記事リンク

はじめに

医療現場では、患者の安全と迅速な治療を支えるため、医療材料・衛生材料の在庫管理が重要です。しかし、小規模な診療所では、依然として手作業での管理が多く、効率化が求められています。
研修先であった下甑手打診療所(Dr.コトー診療所のモデル)にて研修期間中に依頼を受け、医療材料の在庫管理アプリを開発しました。私は20年前に下甑島に住んでおり、当時の院長であった瀬戸上先生(Dr.コトーのモデル)には大変お世話になりました。

現状と課題

現状

  • 施設概要: 病院は2階建てで、1階には窓口、外来、医療資材室があり、2階には病室と透析室があります。
  • 在庫管理の方法: 医療材料は約370種類。以前は紙の台帳で管理していましたが、記入漏れが増加し廃止となりました。現在は在庫の下に赤いカードを設置し、カードが見えたら注文する仕組みを採用しています。
  • 集計作業: 月末に手入力で在庫一覧を集計する作業が必要で、在庫が消費される場所がバラバラなため、正確な在庫の把握が大変です。
資材室の外観 資材室の内観
在庫切れが近い事を示す赤いカード 月末に手入力した在庫一覧

課題

  • 月末の集計レポート作成が大変: 電子カルテと紙カルテが混在しているため、使用数の正確な把握が難しく、帳尻合わせに時間がかかります。
  • 在庫把握が困難: リアルタイムでの在庫把握ができず、在庫を探し出す手間が発生しています。

依頼内容

上記の課題を解決するため、以下の要件を満たす在庫管理アプリの開発を依頼されました。

  • 現在の在庫の把握: QRコードでの入出庫管理を導入。すでに各在庫に貼り付けられたカードにQRコードを追加し、入力ミスを防ぐことが依頼主の強い希望でした。
  • 使いやすさ重視: デジタル端末に不慣れな方でも操作できる設計を重視。
  • 月末レポート出力: 指定した月の在庫変動をレポートとして出力できる機能を実装。

開発要件と選定理由

皆さんスマートフォンをお持ちだったので、各人の端末からWebアプリを利用して、在庫の情報を閲覧・入力して頂く方針としました。

自身に課した開発要件

  • データベースの保全性: Excel操作に慣れている方が簡単に扱えるようにする。
  • 低コスト運用: 小規模開発とし、維持費を抑える。
  • QRコードと手動入力: QRコードの設置コストを抑えつつ、手動入力もサポート。
  • 在庫一覧の実装: 検索機能や並び替え機能を追加し、使いやすさを向上。
  • 履歴管理: 過去の記録を削除でき、入力ミスに対応可能とする。
  • 月別レポート作成: 指定月の入出庫数を自動集計し、レポート形式で出力。

選定とその理由

  • GoogleスプレッドシートとGoogle Apps Script(GAS):
    • 操作の直感性: ユーザーが直感的に操作でき、Excelに慣れた方にとって親しみやすい。
    • データの保全: 印刷が容易で、台風や停電時には紙での管理にも移行可能。
    • 柔軟なレポート作成: スプレッドシート上で編集でき、Excel形式での保存・印刷が可能。

完成したプロダクト

Webアプリ

  • 入力方法: QRコードと手動入力の2種類を提供。QRコードを読み取ることで在庫数を簡単に増減できます。
  • 画面更新機能: 30秒ごとにスプレッドシートから最新情報を取得し、在庫一覧・履歴一覧を更新します。ブラウザのタブを切り替えたときにも更新され、閲覧されていない間は更新を停止。手動更新も可能です。
  • 在庫閲覧: 商品名や商品番号での検索が可能で、最新の在庫状況が一目で確認できます。
  • 修正機能: 履歴の削除ができ、入力ミスにも柔軟に対応可能です。
ホーム画面
手入力 手入力:商品選択後
カメラ入力 カメラ入力:QRコード読み取り後
履歴一覧 履歴一覧:削除時

スプレッドシート

  • 在庫管理タブ: すべてのデータを入力するメインのシートです。
在庫一覧タブ
  • 操作ボタンタブ: 利便性を高めるため、以下のボタンを設置しました。
    • QRコード生成ボタン: 新規商品追加時等に実行し全商品のQRコードを生成します。生成されたQRコードを印刷し、前述の赤いカードに貼り付けて運用する事を想定しています。
    • 月別レポート生成ボタン: 指定月のレポートを簡単に生成します。
    • 再計算ボタン: 在庫管理タブのデータ修正時に在庫数を再計算します。
操作ボタンタブ
  • QRコードタブ: QRコードの生成場所。印刷して前述の赤いカードに貼り付け、在庫管理に活用します。
QRコードタブ QRコード運用イメージ
  • 月別レポートタブ: 前述のボタン操作時に、指定した月の在庫変動を集計したレポートを生成します。
月別レポート

開発に関して

開発期間

開発担当は自分ひとりで約1ヶ月半ほど。週末を中心に開発を進めました。

Webアプリ

構想を考える際には、こちらのサイトが参考になりました。

Google Apps Script内にコードファイル(.gs)とHTMLファイル(.html)を配置し、スプレッドシートとのやり取りを行う関数をコードファイルに記載。HTMLファイルとデータをやり取りする形としました。

Tips

画面サイズの調整について

基本的にスマホで使用する事を想定してるため、画面サイズを対応させる必要がありました。

参考記事:【2021.10.20】GASのWebアプリをスマホでいい感じに表示する方法!

QRコード生成

依頼主の希望に基づき、追加用と減少用のQRコードを別々に作成する方針にしました。
QRコードの生成とセルの大きさの指定、時間制限対策のトリガー設定、QRコード生成の進捗状況の管理を行いました。

暗号化せずシンプルに、商品番号に「_plus1」と「_minus1」を付けたデータをQRコード化しています。

  • 6分の壁を超えるためのトリガー管理に関しては、以下のサイトを参考にしました。

GASでタイムアウトエラーを回避する方法【6分の壁/30分の壁】

Script Serviceによるスクリプトの自動制御 (2/4)■Triggerオブジェクトでトリガー情報を得る

今回使用したScriptApp.getScriptTriggers(); は非推奨になっており、現状サポートされている ScriptApp.getProjectTriggers(); を使う方が望ましいそうです。

繰り返しアイテムの日付プロパティに未来の日時を設定する ~ イチ(以下)から学ぶNotionAPI×Google Apps Script【Day.6】

QRコード生成関数のコード
function generateAllQRCodes() {
  const startTime = new Date(); // 実行開始時刻を記録
  const scriptProperties = PropertiesService.getScriptProperties();
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  const stockSheet = spreadsheet.getSheetByName("在庫管理");
  const qrSheet = spreadsheet.getSheetByName("QRコード");

  // 途中から再開するためのインデックス
  let startIndex = Number(scriptProperties.getProperty("startIndex")) || 0;

  // 最初の実行であればQRコードタブの情報をクリア
  if (startIndex === 0) {
    qrSheet.clear();
    const images = qrSheet.getImages();
    images.forEach((image) => image.remove());
    qrSheet
      .getRange(1, 1, 1, 4)
      .setValues([["番号", "商品名", "QRコード(増加)", "QRコード(減少)"]]);
    qrSheet.setRowHeight(1, 150);
  }

  const lastRow = stockSheet.getLastRow();
  if (lastRow < 2) {
    SpreadsheetApp.getUi().alert("在庫管理シートに商品が存在しません。");
    return;
  }

  const productData = stockSheet.getRange(2, 1, lastRow - 1, 2).getValues();
  const cellSize = 180;

  for (let i = startIndex; i < productData.length; i++) {
    const currentTime = new Date();
    const elapsedSeconds = (currentTime - startTime) / 1000;
    if (elapsedSeconds > 300) {
      // 300秒を超えたら一時停止し、トリガーを設定
      scriptProperties.setProperty("startIndex", i);
      setTrigger();
      return;
    }

    const row = productData[i];
    const productNumber = row[0];
    const productName = row[1];
    const plusCode = productNumber + "_plus1";
    const minusCode = productNumber + "_minus1";

    // エラーハンドリング
    try {
      const plusQRCodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(
        plusCode
      )}`;
      const minusQRCodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(
        minusCode
      )}`;

      const plusQRCodeBlob = UrlFetchApp.fetch(plusQRCodeUrl).getBlob();
      const minusQRCodeBlob = UrlFetchApp.fetch(minusQRCodeUrl).getBlob();
      const currentRow = i + 2;

      qrSheet
        .getRange(currentRow, 1, 1, 2)
        .setValues([[productNumber, productName]]);
      qrSheet.setRowHeight(currentRow, cellSize);

      qrSheet.getRange(currentRow, 3).setBackground("magenta"); // 3列目の背景をマゼンタに設定
      qrSheet.getRange(currentRow, 4).setBackground("cyan"); // 4列目の背景をシアンに設定

      qrSheet.setColumnWidth(3, cellSize); // 3列目の幅を180に設定
      qrSheet.setColumnWidth(4, cellSize); // 4列目の幅を180に設定

      // 画像を挿入
      const imagePlus = qrSheet.insertImage(plusQRCodeBlob, 3, currentRow);
      const imageMinus = qrSheet.insertImage(minusQRCodeBlob, 4, currentRow);

      // QRコードをセルの中央に配置する
      const offset = (cellSize - 150) / 2; 
      imagePlus.setAnchorCellXOffset(offset).setAnchorCellYOffset(offset);
      imageMinus.setAnchorCellXOffset(offset).setAnchorCellYOffset(offset);
    } catch (e) {
      Logger.log(`エラーが発生しました: ${e.message}`);
      scriptProperties.setProperty("startIndex", i);
      setTrigger();
      return;
    }
  }

  deleteTriggerAndProperties();
  SpreadsheetApp.getUi().alert("すべてのQRコードを生成しました。");
}
function setTrigger() {
  // 既存のトリガーを削除
  ScriptApp.getScriptTriggers().forEach((trigger) => {
    if (trigger.getHandlerFunction() === "generateAllQRCodes") {
      ScriptApp.deleteTrigger(trigger);
    }
  });

  // 1分後に再実行するトリガーを設定
  ScriptApp.newTrigger("generateAllQRCodes")
    .timeBased()
    .after(1000 * 60)
    .create();
}

function deleteTriggerAndProperties() {
  // 不要なトリガーを削除
  ScriptApp.getScriptTriggers().forEach((trigger) => {
    if (trigger.getHandlerFunction() === "generateAllQRCodes") {
      ScriptApp.deleteTrigger(trigger);
    }
  });

  PropertiesService.getScriptProperties().deleteProperty("startIndex");
}

QRコード読取

jsQRCDNを用いて実装しました。

在庫一覧表示

検索、項目別の並び替え、表示数の変更機能を実装するためにDataTablesを使用しました。

処理の高速化

データを一括で取得・処理し、まとめて記載することで処理速度を向上させました。
一行ずつ取得・記載する方法ではパフォーマンスが低下するため、以下のように一括処理を行っています。

  • 一括取得
    // 在庫管理シートからデータを一括取得
    const sheet =
      SpreadsheetApp.getActiveSpreadsheet().getSheetByName("在庫管理");
    const data = sheet.getDataRange().getValues();
  • 一括記載
    // 結果を新しいシートに一括記載
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    let monthlySheet = spreadsheet.getSheetByName(yearMonth);
    if (!monthlySheet) {
      monthlySheet = spreadsheet.insertSheet(yearMonth);
    } else {
      monthlySheet.clear(); // シートが存在する場合、内容をクリアして再作成
    }

    // ヘッダーの書き込み
    monthlySheet.getRange(1, 1).setValue(yearMonth);
    monthlySheet.appendRow(["番号", "商品名", "追加数", "消費数"]);

    // データの書き込み
    const outputData = Object.keys(inventoryMap).map((itemNumber) => {
      const item = inventoryMap[itemNumber];
      return [itemNumber, item.name, item.add, item.consume];
    });

    if (outputData.length > 0) {
      monthlySheet.getRange(3, 1, outputData.length, 4).setValues(outputData);
    }  

小話

既存のバーコードではなく、QRコードを利用した理由

医療材料にはGS1バーコードが使用されています。詳細はGS1JAPANが公開している資料にまとまっています。

しかし、無料でGS1バーコードを読み取り、商品情報を取得する方法が見つかりませんでした。
(関連記事:AndroidでGS1データバーをスキャンしようとして諦めた話

また、取得できたとしても、診療所で扱う「単位(箱、個など)」や「発注先」の情報については、別途データを用意する必要があります。
そのため今回は、独自の商品番号を使用し、その番号に対応するQRコードを生成する方針にしました。

在庫データベースの作成

病院の電子カルテにはUSBが挿入できないため、許可を得たうえで、外来のパソコンで在庫一覧のExcelファイルを撮影し、OCRしてCSVファイルを出力し取り込みました。

おわりに

今回のプロジェクトでは、低コストでのDX化に挑戦しました。
今後も医療現場の効率化に貢献できるよう、引き続き努力していきます。

*掲載したコードは開発したものから抜粋・加工したものであり、詳細な解説は割愛しています。

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?