医療現場において低コストでの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コード生成の進捗状況の管理を行いました。
- QRコード生成については、QR Code GeneratorのQR Code APIを利用しています。
暗号化せずシンプルに、商品番号に「_plus1」と「_minus1」を付けたデータをQRコード化しています。
- 6分の壁を超えるためのトリガー管理に関しては、以下のサイトを参考にしました。
GASでタイムアウトエラーを回避する方法【6分の壁/30分の壁】
Script Serviceによるスクリプトの自動制御 (2/4)■Triggerオブジェクトでトリガー情報を得る
今回使用したScriptApp.getScriptTriggers(); は非推奨になっており、現状サポートされている ScriptApp.getProjectTriggers(); を使う方が望ましいそうです。
繰り返しアイテムの日付プロパティに未来の日時を設定する ~ イチ(以下)から学ぶNotionAPI×Google Apps Script【Day.6】)
- 進捗状況の管理は、ScriptPropertiesを使用しています。
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コード読取
在庫一覧表示
検索、項目別の並び替え、表示数の変更機能を実装するために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化に挑戦しました。
今後も医療現場の効率化に貢献できるよう、引き続き努力していきます。
*掲載したコードは開発したものから抜粋・加工したものであり、詳細な解説は割愛しています。