Shoeisha Technology Media

CodeZine(コードジン)

記事種別から探す

Elixir+PhoenixとSpread.Viewsでリアルタイムな出勤管理アプリを作ろう

IoT時代の救世主! SpreadJSで作るデータ可視化アプリ 第2回

  • LINEで送る
  • このエントリーをはてなブックマークに追加

目次

フロント側のコード改修

 ここまでで、サーバーサイドまでICカードの情報が飛んでくるようになりました。

Spread.ViewsのJSコードをES2015化と整理

 次はフロント側に反映させる必要がありますが、その前に一旦Spread.Viewsの実装部分のコードを整理します。BrunchはES2015を推奨しているため、勤務表デモのJSファイルもモダンな書き方に修正していきます。

 カレンダーの処理を記述するassets/js/calender.jsを作成し、もともとassets/js/app.jsに記述してあったコードを移行させます。

 assets/js/app.jsはかなりシンプルになり、assets/js/calender.jsからclassを読み込んで、静的メソッドのcalender.init()を実行する形になります。

assets/js/app.js
import "phoenix_html"
import socket from "./socket"

import Calender from "./calender"
const calender = new Calender();
calender.init(socket);

 本体となるassets/js/calender.jsを以下のように作成します。

assets/js/calender.js
'use strict';

//private メソッド化
const setWindowAction = Symbol();
const updateTitle = Symbol();
const getMonth = Symbol();
const render = Symbol();
const pushData = Symbol();

class Calender {

    constructor(){
        this.CalendarGrouping = {};
        this.DataView = {};
        this.UserData = [];
        this.monthEventTemplate = `<div><div data-column = "card" class="smallIcon"></div></div>`;
        this.presenter = `<img style ="width:100%;height:100%" src="{{=it.card}}"/>`;
        this.columns = [{
            id: 'name',
            caption: 'name',
            dataField: 'name'
        }, {
            id: 'card',
            caption: 'card',
            dataField: 'card',
            presenter: this.presenter
        }, {
            id: 'date',
            caption: 'date',
            dataField: 'date'
        }];

        this.DSMEMBERS = {
            'xxxxxxxxxxx':'n0bisuke',
            'xxxxxxxxxxx':'uko',
            'xxxxxxxxxxx':'chantoku',
            'xxxxxxxxxxx':'chachamaru'
        };
    }

    init(socket){
    	 /*
    	 あとでPhoenix Channelの記述を追加
    	 */
        $(document).ready(this[render]('init')); //初期描画
        this[setWindowAction](this.CalendarGrouping); //dom系処理を有効に
    }

    //データの追加
    [pushData](name, today = 0){
        const now = new Date();
        if(today === 0) today = now.getDate();
        this.UserData.push({
            name: name,
            card: `./images/${name}.png`,
            date: new Date(now.getFullYear(), now.getMonth(), today)
        });
        this[render]();
    }

    //ページ移動系処理
    [setWindowAction](){
        //リサイズ処理
        const resize = () => {
            if (this.dataView) {
                const options = this.dataView.options;
                const strategy = options.groupStrategy;
                const strategyOptions = strategy.options;
                if (strategyOptions.viewMode === "Month") {
                    options.cardHeight = 56;
                    options.cardWidth = 56;
                    options.rowTemplate = this.monthEventTemplate;
                }
                this.dataView.invalidate();
            }
        }

        const previous = () => move(-1); //前へボタン
        const next = () => move(1); //次へボタン

        const move = (step) => {
            const options = this.CalendarGrouping.options;
            let currentDate = options.startDate;
            currentDate = this[getMonth](currentDate, step);
            this.CalendarGrouping.options.startDate = currentDate;
            this[updateTitle]();
            this.dataView.invalidate();
        }

        $('#btn-next').on('click', next);
        $('#btn-previous').on('click', previous);
        $(window).resize(resize);
    }

    //タイトルの更新
    [updateTitle](){
        const options = this.CalendarGrouping.options;
        const date = options.startDate;
        const $title = document.getElementById("title");
        const dateFormatter = new GC.Spread.Formatter.GeneralFormatter('mmmm yyyy');
        $title.innerText = dateFormatter.format(date);
    }

    //月を取得
    [getMonth](currentDate, monthCount){
        const day = currentDate.getDate();
        let year = currentDate.getFullYear();
        let month = currentDate.getMonth() + monthCount;
        if (month == 12) {
            month = 0;
            year += 1;
        } else if (month == -1) {
            month = 11;
            year -= 1;
        }
        return new Date(year, month, day, 0, 0, 0);
    }

    //レンダリング
    [render](flag = ''){

        this.CalendarGrouping = new GC.Spread.Views.Plugins.CalendarGrouping({});
        this.CalendarGrouping.eventLimitClick.addHandler((sender, args) => {
            const options = sender.options;
            options.cardHeight = 100;
            options.cardWidth = 100;
            options.rowTemplate = '<div><div data-column = "card" class="bigIcon"></div></div>';
        });

        this.CalendarGrouping.popoverClose.addHandler((sender, args) => {
            const options = sender.options;
            options.cardHeight = 56;
            options.cardWidth = 56;
            options.rowTemplate = this.monthEventTemplate;
            sender.invalidate();
        });

        if(this.UserData[0] === undefined) return;
        this.dataView = new GC.Spread.Views.DataView(document.getElementById('grid1'), this.UserData, this.columns, new GC.Spread.Views.Plugins.CardLayout({
            cardHeight: 56,
            cardWidth: 56,
            grouping: {
                field: 'date',
                converter: (field) =>  field.toDateString()
            },
            rowTemplate: this.monthEventTemplate,
            groupStrategy: this.CalendarGrouping
        }));
        this[updateTitle]();
    }
}

export default Calender

constructor

 まず、constructorでは利用する内部だけで利用する変数をプロパティ化して設定しています。

 this.DSMEMBERSはdotstudio社の社員のidを記録しています。

 this.UserDataが勤怠情報の大元となるデータで、ここに誰がいつ出勤したのかが格納されていきます。

initメソッド

 このメソッドは静的メソッドで外部から呼び出します。

 ここが全ての処理の起点となるような形となり後ほどPhoenix Channelの処理を追記する箇所になります。

pushDataメソッド

 もとのコードではcreateDataメソッドgetRandomDataメソッドで初期データを作成していましたが、今回はICカードと連動して、動的にデータを追加していきます。pushDataメソッドではthis.UserDataにデータを追加します。

setWindowActionメソッド

 もとのコードのresizeメソッドpreviousメソッドnextメソッドを吸収しています。

 ウィンドウのリサイズ時の処理、来月を表示するボタン、先月を表示するボタンの処理になります。

updateTitleメソッド

 初期表示時や、月を移動した際の表示タイトルを更新します。

getMonthメソッド

 初期表示時や、月を移動した際に月の数字を再計算します。

renderメソッド

 もとのコードのrenderメソッドをそのまま利用しています。

 初期表示時や、pushDataメソッドでデータが追加された際にカレンダーの再描画を行います。

 これで今回のSpread.Viewsの機能をassets/js/caldender.jsにまとめたので、そのほかの処理を追加するときは同様に新規でファイルを作成してClass分けを行うなど更新しやすくなりました。

Phoenix Channelでカレンダーをリアルタイムに反映

 最終ステップです。

 現状だとICカードを読み取ってもフロント側まで情報は反映されません。フロントのコードにPhoenix Channelのコードをフロントエンドに組み込みます。

 assets/js/calender.jsinitメソッドを編集します。

assets/js/calender.js
//省略

    init(socket){
        const chan = socket.channel("shifttable:lobby", {})
        chan.join()
            .receive("ignore", () => console.log("auth error"))
            .receive("ok", (messages) => {
                messages.forEach((msg) => {
                    if(!isNaN(msg.user) && 0 <= msg.user && msg.user <= 31) this[pushData](msg.body, msg.user);
                })
                console.log("join ok")
            })
            .receive("timeout", () => console.log("Connection interruption"))

        chan.on("touch_nfc", card => {
            const now = new Date();
            const today = now.getDate();
            const name = this.DSMEMBERS[card.id];
            //存在チェック
            if(name === undefined){
                console.log(`${name}は存在しません。`);
                return;
            }

            //重複チェック
            let flag = 0;
            for(let i = 0, len = this.UserData.length; i < len; i++){
                if(this.UserData[i].date.getDate() === today && `${this.UserData[i].name}` === name){
                    console.log(`既に${name}さんは${today}日に出勤登録済みです。`);
                    flag = 1;
                }
            }
            if(flag === 0) this[pushData](name, today);
        });

        $(document).ready(this[render]('init')); //初期描画
        this[setWindowAction](this.CalendarGrouping); //dom系処理を有効に
    }

//省略

 まずはsocket.channel("shifttable:lobby", {})の箇所でshifttable:lobbyトピックに接続します。

 chan.onメソッドではtouch_nfcイベントの待受をします。Raspberry Pi側でICカードリーダから情報が発信されるとこの部分の処理が動き、pushDataメソッドに情報が送られthis.UserDataが更新されます。

 また、この際に同じ人が同じ日に出勤記録を打とうとしてないかの重複チェックと、社員じゃない人や登録していないICカードでリーダーに触れた場合の存在チェックも行っています。

 最後にassets/js/socket.jsを編集します。

assets/js/socket.js
import {Socket} from "phoenix"

let socket = new Socket("/socket", {params: {token: window.userToken}})
socket.connect()
export default socket

 以上で完成です。

 このようにICカードリーダーにICカード(動画ではNFC機能内臓フィギュアを利用)を近づけると、サーバーにPhoenix Channel経由で情報が飛び、フロント側でもtouch_nfcイベントを取得して情報をカレンダーに反映させます。


  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • 菅原のびすけ(dotstudio株式会社)(スガワラ ノビスケ)

     日本最大規模のIoTコミュニティ「IoTLT」主催。岩手県立大学大学院ソフトウェア情報学研究科を卒業後、株式会社LIGでWebエンジニアとして入社し、Web開発に携わる。2016年にdotstudio株式会社を立ち上げ、今はIoT領域を中心に活動している。JavaScript Roboticsコミ...

バックナンバー

連載:IoT時代の救世主! SpreadJSで作るデータ可視化アプリ
All contents copyright © 2005-2018 Shoeisha Co., Ltd. All rights reserved. ver.1.5