PleasanterとQR読取りを組み合わせた物品情報管理

## はじめに
Pleasanterは、ノーコード・ローコード開発ツールとして、様々なビジネスシーンでの情報管理に活用されています。そのカスタマイズ機能の中でも、スクリプト機能を使うことで独自の機能追加が可能です。  
物品管理や棚卸しといった業務では、管理対象の物品に固有のバーコードやQRコードを付与し、これを読み取ることで関連情報を取得・記録したいというニーズが多くあります。  
本記事では、Pleasanterのスクリプト機能と、JavaScriptでブラウザからカメラを使ってQRコードを読み取れるライブラリ html5Qrcode を組み合わせ、QRコードに埋め込まれたPleasanter上のレコードIDをもとに、該当するレコード情報を取得し、画面項目に自動的に設定する一例をご紹介します。

### 動作イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/13dac305-bf1b-4dbc-9635-eefd4e18728c.png)


本記事でのデモでは、対象のPleasanterサイト（**棚卸し登録**）の新規作成画面にQRコード読み取り用の領域が表示されます。  QRコードを読み取り領域にかざすと、QRコードの内容からレコードIDを自動的に抽出し、抽出したレコードIDをもとに、PleasanterのAPIを使用して該当するレコードの情報を取得し、棚卸し登録サイトの特定の項目に自動で設定します。

## 事前の準備
### Pleasanterの動作環境
今回のデモではPleasanterのバージョンは ver.1.4.15.0 を使用しています。

:::note warn
PleasanterWEBサービス起動時の注意事項
今回のデモではスマートフォンやタブレットのカメラを使用することを想定しています。この場合、ブラウザのセキュリティ要件により、保護されたコンテキスト（HTTPS や localhost）からしかカメラを利用できません。
このため、PleasanterのWEBサービスは **HTTPS**プロトコルで利用できるよに起動にする必要があります。
:::

### 使用するライブラリ
ブラウザ上でカメラを利用してQRコードの読み取りを行うために、JavaScriptライブラリである **html5Qrcode** を使用します。

https://github.com/mebjas/html5-qrcode

### 読取り対象となるQRコード
ビジネス用途で利用される高機能なシールプリンター・ラベルプリンターでは、CSVによる一括データ読み込みとともに、QRコードの生成機能も内包していることが一般的なため、本デモではQRコード自体の生成は想定していません。今回のデモでは、ラベルプリンタの機能で生成した以下のようなQRコードを利用しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/1ca91772-1cee-46aa-8c77-ccd0df27b2dd.png)
今回のデモでは、後述する **物品マスタ**のレコードID（例：`https://[PleasanterのURL]/items/123456` のようなURL形式）をQRコードにエンコードすることを想定しています。 
このような内容とすることで、このデモでの棚卸し登録の利用のほか、その他のシーンでもQRコードを読み込むことでラベルに記載された内容よりも詳しい物品情報を参照することができます。

## Pleasanterの各サイト設定 
### サイトの作成と画面項目の設定
QRコードの情報をもとに他サイトのレコード情報を取得するため、以下の3つのサイトを作成します。

|サイト|説明|
|---|---|
|場所マスタ|管理する物品の場所情報を管理するマスタ|
|物品マスタ|管理する物品の情報を管理するマスタ|
|棚卸し登録|棚卸しの情報を登録するサイト|
#### 場所マスタ
後述の**物品マスタ**、**棚卸し登録** 2つのサイトから参照される場所情報のマスタです。

作成したサイトとデータの例:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/d48f6993-4297-429f-a84e-54fceb534f9c.png)
※マスタ情報のみで特別な設定はありませんので詳細は割愛します。
___
#### 物品マスタ
**棚卸し登録** のサイトから参照される物品情報を管理するマスタです。

作成したサイトとデータの例:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/58c84468-e64d-481a-9aa2-afd0ef211094.png)
#### 物品マスタの項目設定

物品マスタ項目のうち、他サイトと関連のある主要な項目は以下のように設定します。
|表示名|項目名|説明|
|---|---|---|
|場所|ClassA| **場所マスタ**へのリンクを設定|


今回のデモでは、この **物品マスタ**のレコードID（例：`https://[PleasanterのURL]/items/123456` のようなURL形式）をQRコードにエンコードすることを想定しています。 
___
 #### 棚卸し登録
QRコードを読み取った結果（物品マスタのレコードIDなど）を登録するためのサイトです。
本項でこれ以降の説明にあるサイトの設定はすべてこの**棚卸し登録**サイトへ行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/ff89a18f-43e4-4583-9516-a9af3b568622.png)

 #### 棚卸し登録の項目設定
 棚卸し登録項目のうち、他サイトと関連のある主要な項目は以下のように設定します。
|表示名|項目名|説明|
|---|---|---|
|棚卸し場所|ClassA| **場所マスタ**へのリンクを設定|
|物品名|ClassB|**物品マスタ**へのリンクを設定<br>棚卸し対象となる物品の情報をQRコードから取得|
#### HTMLヘッダーにHTMLを挿入
**棚卸し登録**サイトのテーブルの管理画面から、 `HTML` を選択し、挿入位置を`Head bottom` に指定して、以下のコードを貼り付けます。  出力先は `新規作成` と `編集` を指定します。
```html
<!-- html5Qrcode ライブラリをCDNから読み込み -->
<script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
```
#### スタイルの設定
**棚卸し登録**サイトのテーブルの管理画面から、 `スタイル` を選択し、以下のコードを貼り付けます。  出力先は `新規作成` と `編集` を指定します。
```css
/* QRリーダー要素 */
#reader {
  display: none; /* 初期状態は非表示 */
  width: 100%;
  max-width: 500px;
  height: auto;
  margin: 10px auto;
}
```

#### スクリプトの設定
**棚卸し登録**サイトのテーブルの管理画面から `スクリプト` を選択し、以下のコードを貼り付けます。出力先は`新規作成`、`編集` を指定します。
```javascript
// html5Qrcode の初期設定
const config = {fps: 2, qrbox: { width: 250, height: 250} }; // スキャン範囲のサイズ
let html5Qrcode = null;

//QRコード読み取り成功時の処理
function qrCodeSuccessCallback(decodedText, decodedResult) {
    //URLから対象のレコードIDを抽出
    const idMatch = decodedText.match(/\/items\/(\d+)/);
    if (idMatch && idMatch[1]) {
        const extractedId = idMatch[1];
        checkRecordId(extractedId);
    } else {
        $p.clearMessage();
        $p.setMessage('#Message', JSON.stringify({
            Css: 'alert-warning', 
            Text: `読取り結果から有効なレコードIDを抽出できませんでした。${decodedText}`
        }));
    }
}

//抽出したIDでレコード存在確認、物品マスタに存在する有効なレコードIDであればClassBにセット
function checkRecordId(recordId) {
    $p.apiGet({
        id: recordId,
        done: function (data) {
            if (data?.Response?.Data?.length > 0) {
                if ($p.getControl("ClassB").val() !== recordId) {
                    $p.set($p.getControl("ClassB"), recordId);
                    $p.clearMessage();
                    $p.setMessage('#Message', JSON.stringify({ 
                        Css: 'alert-success', 
                        Text: `物品マスタ情報 (ID: ${recordId}) を取得しました。`
                    }));
                } 
            } else {
                $p.clearMessage();
                $p.setMessage('#Message', JSON.stringify({ 
                    Css: 'alert-warning', 
                    Text: `指定ID (${recordId}) の物品マスタレコードが見つかりませんでした。`
                }));
            }
        },
        fail: function (xhr, status, error) {
            $p.clearMessage();
            $p.setMessage('#Message', JSON.stringify({ 
                Css: 'alert-error', 
                Text: `物品マスタからの情報取得に失敗しました。 ${status} ${error}`
            }));
        }
    });
}

//スキャナーUI要素を表示し、スキャンを開始
async function startScanner() {
    try {
        // カメラアクセスを要求 (facingMode を指定して背面カメラへのアクセスを試みる)
        const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } });
    } catch (permissionError) {
        // getUserMedia が失敗した場合 (権限がない、カメラがない、その他のエラー)
        $p.clearMessage();
        $p.setMessage('#Message', JSON.stringify({ 
            Css: 'alert-error', 
            Text: `カメラの起動に失敗しました。(${permissionError.name})` 
        }));
        // 権限がない、またはエラーが発生した場合はスキャナーを開始せずに終了
        return;
    }

    // Reader Divを表示
    $(`#reader`).show();

    // 編集モードでボタンから開始した場合、ボタンを非表示
    if ($p.action() === 'edit') {
        $('#action-button').hide();
    }
    // スキャン開始
    try {
        await html5Qrcode.start({ facingMode: "environment" },config,qrCodeSuccessCallback);
    } catch (error) {
        $p.clearMessage();
        $p.setMessage('#Message', JSON.stringify({
            Css: 'alert-error',
            Text: 'カメラを開始できませんでした。ブラウザのカメラ権限を確認してください。' 
        }));
        //失敗した場合、Readerを隠し、編集モードならボタンを再表示 
        $('#reader').hide();
        if ($p.action() === 'edit') {
            $('#action-button').show();
        }
    }
}

//「編集画面」を読み込んだときに実行するメソッド
$p.events.on_editor_load = function () {

    // 読み取り領域のDivをClassB要素のあとに非表示状態で追加
    $('#Results_ClassB').after('<div id="reader"></div>').next('#reader').hide();

    // Html5Qrcode オブジェクトを初期化
    if (!html5Qrcode) {
        try {
            // Html5Qrcodeを初期化
            html5Qrcode = new Html5Qrcode("reader", {formatsToSupport: [ Html5QrcodeSupportedFormats.QR_CODE],verbose: false});
        } catch (initError) {

             $p.clearMessage();
             $p.setMessage('#Message', JSON.stringify({ 
                Css: 'alert-danger', 
                Text: 'QRコードリーダーの準備に失敗しました。'
            }));
             if ($('#action-button').length > 0) {
                 $('#action-button').button("disable");
             }
             return; // 初期化失敗時は以降の処理を中断
        }
    }

    if ($p.action() === 'new') {
        // 新規作成時: 自動でスキャン開始
        startScanner();
    }else{
        // 編集時: ボタン準備
        if ($('#action-button').length === 0) {
            const qrButton = $('<button id="action-button">QR読取</button>')
                .button({ icon: 'ui-icon-video' })
                .on('click.qrscan', startScanner); // クリックでスキャン開始
            $('#Results_ClassB').after(qrButton);
        }
        // 初期状態ではReaderは非表示
        $('#reader').hide();
        // ボタン表示
        $('#action-button').show();
    }

};

```
上記のスクリプトは、Pleasanterのクライアントサイドで動作し、html5Qrcodeライブラリを利用してQRコードの読み取り機能を実現します。読み取ったQRコードの内容からレコードIDを抽出し、PleasanterのAPIを使ってそのレコード情報を取得・表示中の画面項目に設定します。

以下、スクリプトの主要な処理を説明します。

**`$p.events.on_editor_load`**  
この関数は、Pleasanterの編集画面がブラウザに読み込まれたタイミングで実行され、QRコードリーダー読取り処理や画面の初期設定を行います。 
画面が新規作成の場合は、自動でQRコードのスキャンを開始します。  画面が編集の場合は、物品データは登録済みのため、ボタンを押下した場合にQRコードのスキャンを開始します。

**`startScanner()`**  
この非同期関数は、ブラウザからカメラを起動してQRコードのスキャンを開始します。
`navigator.mediaDevices.getUserMedia` を利用して、ブラウザからカメラへのアクセス許可をユーザーに求め、`html5Qrcode.start()` メソッドを呼び出し、スキャンを開始します。

**`qrCodeSuccessCallback(decodedText, decodedResult)`**  
`html5Qrcode` によってQRコードが正常に読み取られるたびに実行される関数です。読み取られたQRコードのテキストデータから、Pleasanterのレコード詳細URLに含まれる数字文字列を抽出し、有効なレコードIDが得られた場合は、そのIDを引数として `checkRecordId` 関数を呼び出します。

**`checkRecordId(recordId)`**
`qrCodeSuccessCallback` から抽出されたレコードIDを受け取り、そのIDに対応するPleasanterレコードの情報を取得する関数です。
PleasanterのスクリプトAPIである `$p.apiGet` メソッド を使用し、レコードIDをもとに物品マスタの情報を取得し、現在編集中の棚卸し登録レコードの`ClassB`項目に値を設定します。

## 操作の実行例

上記の設定を完了した後、以下のように操作することでQRコード読み取り機能を利用できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/607907bb-a009-43bb-be8f-aa5f0310975d.png)

1.  QRコードリーダー機能を組み込んだ **棚卸し登録**サイトで新規レコードを開きます。
2.  ブラウザがカメラへのアクセス許可を求めてきたら、許可を選択します。
3.  カメラが起動し、画面にQRコードの読み取り領域が表示されます。
4.  物品マスタサイトの特定のレコードのURL（例：`/items/123456` を含むURL）がエンコードされたQRコードを、画面上の読み取り領域にかざします。
5.  QRコードが正常に読み取られると、コードからレコードIDが抽出され、登録済みの物品情報が自動で設定されます。
6.  その他、物品を確認した棚卸し場所や、物品の状態など選択し、データを登録します。

## まとめ

本記事では、Pleasanterのスクリプト機能と外部JavaScriptライブラリであるhtml5Qrcodeを連携させることで、QRコードの読み取りからPleasanter上のレコード情報取得、そして画面項目への自動設定までを一連の流れとして実現する方法をご紹介しました。

この仕組みを利用することで、物品のIDを手動で入力する手間を省き、棚卸しや物品の貸し出し・返却登録といった業務において、正確かつ効率的なデータ入力が実現できます。

今回紹介したレコードIDの設定だけでなく、取得した物品マスタの他の項目（例：保管場所や状態など）を読み取って関連する画面項目に自動転記するといった応用的なカスタマイズも可能です。