ウェブでレジ入力機能をつくる

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、長くブログを続けていると「うーん、たまには味変した記事が書きたい」なんて思うことがあります。

というのも、2020年1月27日現在、350 を超える記事を書いているのですが、言っても毎回毎回まったく違う内容というわけではないからなんですね😂

そして、そんな過去の記事のことを考えていると、ふとある「モノ」が頭に浮かびました。

そういえば、昔買ったUSBバーコード・リーダーはどこいったんだろう??

📝参考記事: 【Laravel】バーコード・スキャナーでIBSNを読み取って本の入荷処理をする

と。

そこで❗

今回は、棚の奥の奥に埋まっていたバーコード・リーダーを使って、いつもとは違うテーマをお届けしたいと思います。

そのテーマはこちら。

ウェブページでレジ入力機能をつくる

です。

今回は、ぜひ自由研究的な感覚で読んでいただけると嬉しいです😂
(最後に、今回開発したソースコード一式をダウンロードできますよ👍)

「Flutter を2日かかってインストールしました😫
Linuxには厳しいですね・・・」

開発環境: Laravel 8.x、Vue 3、axios 0.19、Lodash 4.17、TailwindCSS 2

やりたいこと

よくスーパーなどで見かけるレジ打ちをUSBバーコード・リーダーを使って実装してみます。

流れは、次のとおりです。

  1. バーコードを読み取る
  2. Ajaxで自動的に商品データを取得
  3. その商品を一覧に表示する

これだけでは面白くないので、さらに以下の機能も使えるようにしてみます。

  • すでに読み取った商品の場合は個数をプラス1する
  • 個数は変更できる
  • 削除もできる
  • 合計を税込みで計算する
  • フォーカスがブラウザから外れたら警告をだす

では楽しくやっていきましょう❗

ルートをつくる

では、まずルートです。
必要となるのは以下の2つです。

  • 実際のブラウザでアクセスするレジ入力ページ
  • Ajaxで商品データを取得するページ

routes/web.php

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\CashRegisterController;

Route::get('cash_register', [CashRegisterController::class, 'index']);
Route::get('cash_register/{jan_code}', [CashRegisterController::class, 'show']);

コントローラーをつくる

続いて、ルートの中で指定したコントローラーを作成します。
以下のコマンドを実行してください。

php artisan make:controller CashRegisterController

するとファイルが作成されるので、中身を以下のように変更します。

app/Http/Controllers/CashRegisterController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class CashRegisterController extends Controller
{
    public function index() {

        return view('cash_register.index');

    }

    public function show($jan_code) {

        $items = [
            [
                'title' => 'UXデザインをはじめる本',
                'jan_code' => '9784798143330',
                'price' => 2200,
                'tax_percent' => 10
            ],
            [
                'title' => 'ノンデザイナーズ・デザインブック',
                'jan_code' => '9784839928407',
                'price' => 2000,
                'tax_percent' => 10
            ],
            [
                'title' => '「納品」をなくせばうまくいく',
                'jan_code' => '9784534051943',
                'price' => 1600,
                'tax_percent' => 10
            ]
        ];
        $item = collect($items)->first(function($item) use($jan_code){

            return ($item['jan_code'] === $jan_code);

        });

        return [
            'result' => !empty($item),
            'item' => $item
        ];

    }
}

見ていただいているとおり、今回商品データは直接配列として定義していますが、本番環境の場合はDBから呼び出すようにしてください。

また、first()内の意味としては、「送信されてきたJANコードを持っているデータを取得する」となります。

ちなみに、コレクションには他にもたくさん便利なメソッドがありますので、興味がありましたら以下のページをご覧ください。

📝 全117種類!Laravel 5.6〜7.xのコレクション実例

ビューをつくる

では、最後にメインのビューになります。
以下のファイルを作成してください。

resources/views/cash_register/index.blade.php

<html>
<head>
    <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
</head>
<body>
<div id="app" class="p-5">
    <h1 class="text-3xl font-bold text-yellow-800 mb-5">ウェブでレジ入力機能・サンプル</h1>
    <div class="my-5" v-if="!focusing">
        <span class="bg-red-500 text-red-50 rounded p-3 font-bold">&#x26A0; JANコードを入力できません。画面にフォーカスしてください。</span>
    </div>
    <table class="borer border-2 border-gray-200">
        <thead class="bg-blue-200">
            <tr>
                <th class="p-2">商品名</th>
                <th class="p-2">価格</th>
                <th class="p-2">数量</th>
                <th class="p-2">数量変更</th>
                <th class="p-2">削除</th>
            </tr>
        </thead>
        <tbody class="bg-gray-50">

            <!-- ① 読み込まれた商品の一覧を表示する -->
            <tr v-for="item in items">
                <td class="p-2" v-text="item.title"></td>
                <td class="p-2" v-text="item.price"></td>
                <td class="p-2 text-center" v-text="item.quantity"></td>
                <td class="p-2 text-center">
                    <button class="bg-blue-500 px-2 py-1 text-blue-50 rounded ml-4" type="button" @click="onEdit(item)">変更</button>
                </td>
                <td class="p-2 text-center">
                    <button class="bg-red-500 px-2 py-1 text-red-50 rounded" type="button" @click="onDelete(item)">削除</button>
                </td>
            </tr>

            <!-- ② 価格の合計金額を表示する -->
            <tr>
                <td class="p-3 font-bold text-3xl text-right" colspan="5">
                    <span v-text="totalPrice"></span>円
                    <span class="text-sm font-normal text-gray-500">(税: <span v-text="totalTax"></span>円)</span>
                </td>
            </tr>

        </tbody>
    </table>

    <!-- ③ JANコードの入力ボックス -->
    <input
        ref="janCodeInput"
        style="position:absolute;top:-100px;left:-100px;"
        type="text"
        v-model="janCode"
        @input="onInput"
        @focus="onFocus"
        @blur="onBlur">

</div>
<script src="https://unpkg.com/vue@3.0.2/dist/vue.global.prod.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
<script>

    Vue.createApp({
        data() {
            return {
                focusing: false, // <input> が入力状態かどうか
                submitting: false, // Ajax送信中かどうか
                janCode: '',
                items: [], // 読み取り済みのJANコード
            }
        },
        methods: {
            getItem(janCode) {  // ④ Ajaxで商品データを取得する

                this.submitting = true; // 「送信中」に変更
                const url = `/cash_register/${janCode}`;
                axios.get(url)
                    .then(response => {

                        if(response.data.result === true) {

                            const item = response.data.item;
                            this.addItem(item);

                        } else {

                            alert('この商品は存在しません...');

                        }

                    })
                    .catch(error => {

                        alert('予期しないエラーが発生しました...');

                    })
                    .finally(() => { // 成功しようが失敗しようが必ず通る場所

                        this.submitting = false; // 送信状態を初期化
                        this.janCode = ''; // JANコードを初期化

                    });

            },
            onInput() { // ⑤ 読み取ったJANコードが入力されたときに実行

                if(this.submitting === false) {

                    const janCode = this.emToEn(this.janCode);

                    if(janCode.match(/^[0-9]{13}$/)) { // 半角数字が13文字の場合だけ実行する

                        this.getItem(janCode);

                    }

                }

            },
            onFocus() { // ⑥-1 入力ボックスが入力状態になったとき

                this.focusing = true;

            },
            onBlur() { // ⑥-2 入力ボックスが入力状態ではなくなったとき

                this.focusing = false;
                this.janCodeInput.focus(); // すぐ入力状態へ戻す

            },
            onDelete(item) { // ⑦ 削除するボタンがクリックされたとき

                const message = `『${item.title}』を削除します。よろしいですか?`;

                if(confirm(message)) {

                    _.remove(this.items, {
                        jan_code: item.jan_code
                    })

                }

            },
            onEdit(item) { // ⑧ 個数の変更ボタンがクリックされたとき

                let newQuantity = prompt('件数を変更してください。', item.quantity);
                newQuantity = parseInt(
                    this.emToEn(newQuantity)
                );

                if(newQuantity > 0) {

                    item.quantity = newQuantity;

                }

            },
            addItem(addingItem) {

                const item = _.find(this.items, { // 同じJANコードの商品を探す
                   jan_code: addingItem.jan_code
                });

                if(item !== undefined) { // すでに同じ商品が読み取られている場合

                    item.quantity++;

                } else { // 新しく読み取られた場合

                    addingItem['quantity'] = 1;
                    this.items.push(addingItem);

                }

            },
            emToEn(str) { // 全角数字を半角へ変換

                return str.replace(/[0-9]/g, s => {

                    return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);

                });

            }
        },
        computed: {
            // ⑨ 合計金額を計算する
            totalPrice() {

                return _.sumBy(this.items, item => {

                    return item.price * item.quantity;

                }) + this.totalTax;

            },
            totalTax() {

                return _.sumBy(this.items, item => {

                    return Math.floor(item.price * item.tax_percent * item.quantity * 0.01);

                });

            }
        },
        mounted() {

            this.janCodeInput.focus(); // <input> を入力状態にする

            // テストのちょっとしたTip
            // this.getItem('9784534051943');
            // this.getItem('9784839928407');

        },
        setup() {

            return {
                janCodeInput: Vue.ref(null) // ⑩ <input ref="janCodeInput"...> にアクセスできるようにする
            }

        }
    }).mount('#app');

</script>
</body>
</html>

では、コードが長いので順を追って紹介していきます。

① 読み込まれた商品の一覧を表示する

USBバーコードリーダーでJANコードを読み取ると、Ajaxで商品データを取得することになりますが、その全データがここに表示されることになります。

なお、1行ごとに「変更」と「削除」ボタンが表示されることになります。

② 価格の合計金額を表示する

商品データの値段と消費税の合計を表示する部分です。
計算部分は、「⑨ 合計金額を計算する」でご紹介します。

③ JANコードの入力ボックス

ここは少し変化球な部分です。

なぜなら、この入力ボックスは「表示するけど見せない」ものだからです。
そのため、スタイルシートは以下のようになります。

style="position:absolute;top:-100px;left:-100px;"

なぜなら、この入力ボックスはバーコードリーダーのためにだけ用意しているので、直接操作されると都合が悪いからです。そのため、強制的に画面外へ移動させているんですね。

では、なぜclass"hidden"として表示自体を消してしまわないかというと、それでは入力自体ができなくなってしまい、もちろんVue側にもデータが伝わらないからです。

なお、この入力ボックスには以下3つのメソッドがイベントとしてセットされています。

  • onInput: 入力した時
  • onFocus: フォーカスが当たったとき
  • onBlur: フォーカスが外れたとき

④ Ajaxで商品データを取得する

getItem()の中では、Ajax送信で商品データを取得しています。
アクセスしているのは、CashRegisterControllershow()です。

なお、途中でaddItem()が呼ばれていますが、このメソッドの中では以下2つの条件で処理を変えています。

  • まだ一覧に表示されていない商品の場合: this.items へ新規追加
  • すでに一覧にある場合: その個数だけをプラス1する

なお、getItem()を呼び出しているのは、onInput()の中だけです。

そのため、直接その中にコードを書いてもいいのですが、今回はあえてgetItem()として分離しました。

なぜかというと、「テストがしやすいから」です。

mounted()の中を見てください。
以下のようなコメントアウトされた部分があるはずです。

// テストのちょっとしたTip
// this.getItem('9784534051943');
// this.getItem('9784839928407');

つまり、テストのときはここをコメントアウトするだけでいちいちバーコードを読み込まなくてよくなるんですね。(最初はピッピと音をならしながら読み取ってましたが、途中でめんどうになってこの形にしました)

このように、開発や保守のために構造を変化させておくことも「ラク」できるテクニックのひとつだと思います👍

⑤ 読み取ったJANコードが入力されたときに実行

USBバーコードリーダーは、形が違うだけでほぼキーボードと同じ動きをします。つまり、onInput()は文字列の数だけ呼ばれることになります。

しかし、そうなってしまうと一文字追加されるごとにAjax送信することになり、時間がかかってしまうかもしれません。

そのために、以下のように正規表現を使って「半角数字が13桁の場合だけ送信」しています。

if(janCode.match(/^[0-9]{13}$/)) { // 半角数字が13文字の場合だけ実行する

    this.getItem(janCode);

}

⑥ 入力ボックスが入力状態になった/ではなくなったとき

まずonBlurJANコードの入力ボックスからフォーカスが外れた(入力状態ではなくなった)ときに実行される部分です。

その場合、自動的にフォーカスが戻る(入力状態に戻す)ようにしています。

そして、onFocusonBlurともに実行されたときにfocusingという変数にtrue or falseを格納されるようにしています。

これは、もしフォーカスが外れた(例えばブラウザから出て、他のアプリケーションにフォーカスが当たった場合など)場合に以下の警告を出すためです。

<div class="my-5" v-if="!focusing">
    <span class="bg-red-500 text-red-50 rounded p-3 font-bold">&#x26A0; JANコードを入力できません。画面にフォーカスしてください。</span>
</div>

⑦ 削除するボタンがクリックされたとき

商品一覧に表示されている「削除」ボタンがクリックされたときに実行される部分です。

以下のようにLodash.jsを使えば簡単に該当するデータを削除することができます。_.***()lodashです。

_.remove(this.items, {
    jan_code: item.jan_code
})

なお、今回はlodashをいくつか利用しています。
他のメソッドにも興味のある方は以下からどうぞ👍

📝 Lodashの便利なメソッド7選!

⑧ 個数の変更ボタンがクリックされたとき

商品の個数を変更する部分です。

本来はモーダルなどで電卓用のような入力を用意すべきなのでしょうが、複雑になりすぎてしまうためprompt()を使いました。(ホント、久しぶりに使いました😂)

⑨ 合計金額を計算する

totalPrice()totalTax()は、それぞれ金額と税額の合計を計算するためのものです。

なお、ここはcomputedの中に入っているので、以下のように「変数のように」利用することができます。

<span v-text="totalPrice"></span>円

⑩ <input ref=”janCodeInput”…> にアクセスできるようにする

ここは、Vue 2で言うところのthis.$refsに当たる部分です。
Vue 3からはこのような書き方に変更になっています。

こうすることで、簡単に入力ボックスの要素を取得できるので以下のような使い方ができるようになります。

this.janCodeInput.focus(); // 入力ボックスにフォーカスを当てる

なお、Vue 2Vue 3の変更点は以下にまとめています。

📝 Vue 3の新しい機能と変更点・全11件

テストしてみる

では、実際にテストしてみましょう❗

まずは、「https://*******/cash_register」にブラウザでアクセスします。
すると、まだ商品は読み込まれていないので、以下のような表示になります。

では、この状態でバーコードを読み取ってみましょう。(もちろん、CashRegisterControllerに登録したJANコードです)

すると、商品と合計金額が表示されました。
では、次に別の商品も追加してみましょう。

はい❗
うまく追加され、合計金額も変更になりました。

では、あえてこの状態で同じ商品を読み込んでみましょう。
すると・・・・・・

商品がの数量が2に変更になりました。
では、今度は手動でこの数量を5に変更してみましょう。

変更」ボタンをクリックします。

すると、プロンプトが表示されますので、この数字を5にして「OK」ボタンをクリックしてみましょう。

どうなるでしょうか・・・・・・

はい❗
数量が5に変更になりました。

では続いて商品の削除です。
「納品」を… の行にある「削除」ボタンをクリックしてみましょう。

すると、確認ポップアップが表示されますので、「OK」ボタンをクリックしてみます。

すると・・・・・・

はい❗
該当のデータが削除されました😊✨

では、最後にブラウザからフォーカスを外して警告がでるのかをチェックしておきましょう。

はい❗
全て想定通りに動いています。
やりました😊👍✨

ダウンロードする

今回実際に開発したソースコード一式を以下からダウンロードできます。

ウェブでレジ入力機能をつくる

※ CDNを使っているので、Laravelにセットしたらすぐ使えますよ👍

おわりに

ということで、今回は棚の奥で眠っていたUSBバーコード・リーダーを使ってレジ入力をつくってみました。

個人的に「デジタルとリアルの融合」したシステムはとても好きなので、楽しく作業をすることができました。

そういえば、棚の奥にはまだ「ラズベリーパイ」が眠ってました・・・まだ箱から空けてもいない😂

いつか一緒に遊ぼうね👍

ではでは〜❗

「個人開発について
いろいろ考えています
(企画、ヘタだけど…)」

今回の記事に関連するご依頼、お待ちしております😊✨ お問い合わせ
また、以下のお問い合わせもお待ちしております!
  • 個人レッスン
  • メールサポート: 詳細
  • ツイッターのフォロー: 詳細
  • 投げ銭のご支援: 詳細
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly