LoginSignup
1
3

More than 3 years have passed since last update.

Monacaとニフクラ mobile backendを使って勤怠管理アプリを作ろう【ハンズオンスライド】

Last updated at Posted at 2020-10-19
1 / 109

ベースアプリについて


ハンズオンのベースアプリはMonacaプロジェクトのインポートより取得できます。

出退勤アプリ


1. 認証機能を実装する


index.html について

index.htmlは一番初めに読み込まれるHTMLファイルです。ここではOnsen UIのタブバーを読み込んでいます。初期設定は active 要素で指定されている通り、home.htmlを表示します。


<ons-toolbar>
  <div class="center">出退勤管理</div>
</ons-toolbar>
<ons-page>
  <ons-tabbar swipeable position="auto" id="tabbar">
    <ons-tab page="home.html" label="出退勤" icon="home, material:md-home" active>
    </ons-tab>
    <ons-tab page="history.html" label="履歴" icon="history">
    </ons-tab>
    <ons-tab page="config.html" label="設定" icon="md-settings">
    </ons-tab>
  </ons-tabbar>
</ons-page>  

home.html

では最初に読み込まれるhome.htmlについてです。HTMLは出退勤時に押すボタンを表示しているのと、ほかに出勤ボタンを押した同僚の人たちを表示します。


<div style="text-align: center; padding-top: 3em">
  <p>
    <!-- 現在時刻を表示します -->
    <strong>現在時刻:<span id="time"></span></strong>
  </p>
  <p>
    <!-- あなたの名前が出ます -->
    <strong><span id="name"></span>さん(ログイン中)</strong>
  </p>
  <!-- 出退勤時に押すボタンです -->
  <ons-button id="record">出勤を記録する</ons-button>
  <p>
    <ons-list>
      <ons-list-header>出勤中のみなさん</ons-list-header>
      <!-- 出勤中の人たちを表示 -->
      <span id="workers">
      </span>
    </ons-list>
  </p>
</div>

history.html

自分の出退勤履歴を表示する画面です。こちらもJavaScriptは実装時に説明します。


<p style="padding-top: 5em">
  <ons-list>
    <ons-list-header>履歴(勤務時間)</ons-list-header>
    <span id="records"></span>
  </ons-list>
</p>

config.html

名前を設定する画面です。


<div style="text-align: center; margin-top: 5em;">
  <strong>名前を入力してください</strong>
  <p>
    <ons-input id="name" modifier="underbar" placeholder="出勤花子" required float></ons-input>
  </p>
  <p>
    <ons-button id="save">確定</ons-button>
  </p>
</div>

mBaaSの設定について

ニフクラ mobile backendの設定について解説します。まずアカウントはすでに持っているという前提で進めていきます。その際、アプリケーションキーとクライアントキーが取得できるはずです。そのキーを js/app.js に書き込みます。


// ↓ アプリケーションキーとクライアントキーをそれぞれ書き換えてください
const applicationKey = 'YOUR_APPLICATION_KEY';
const clientKey = 'YOUR_CLIENT_KEY';
window.ncmb = new NCMB(applicationKey, clientKey);

匿名認証の設定

今回はIDやパスワードを使わない、匿名認証を利用します。管理画面の アプリ設定 > 基本 の中にある匿名設定を有効にして保存します。


管理アカウントの作成

データへのアクセス制限を設けるため、管理者アカウントを作成します。会員管理へ移動して、ロールの作成を選びます。新しいロールとして admin を作成します。adminというロールが作成されたら、新しい会員を作成します。ID/パスワードが自由に決めてください。


認証状態の判定

最初に表示されるのは home.html なので、ここで認証判定を行うのがよさそうです。この判定はログアウト処理や履歴管理表示した後も実行したいので、 ons.getScriptPage().onShow に実装するのがよいでしょう。この関数は画面が表示される度に呼び出されます。


最初、次のようになっているはずです。この 1. ログインしていなければ、名前を登録する画面に遷移します の下に処理を書いていきます。


// この画面が表示される度に実行されます
ons.getScriptPage().onShow = async function(e) {
  const page = this;
  // 1. ログインしていなければ、名前を登録する画面に遷移します
  // 現在時刻を表示する処理
  callTimer(page);
  // 出退勤ボタンのラベル変更
  changeStatus(page);
  // 他の働いている人たちの一覧表示
  showWorkerList(page);
}

ログイン状態を判別する

ログイン状態の判別は、NCMBの会員管理機能を使って ncmb.User.getCurrentUser() を取得して行います。これが空の場合、認証されていません。認証されていない場合は config.html に遷移します。 setActiveTab(2) はOnsen UIのタブバーの指定です(一番左が0で次が1になります)。


ログインしている場合は、名前を #name に表示します。名前は user.get('name') で取得できます。なお、nameは今回指定している名称で、皆さんが自由に決められます。
以下のコードを 1. ログインしていなければ、名前を登録する画面に遷移します の下に記述してください。


// ログインユーザを取得します
const user = ncmb.User.getCurrentUser();
if (!user) {
  // ログインしていないので config.html に遷移する
  document.querySelector('#tabbar').setActiveTab(2);
  return;
}
page.querySelector('#name').innerHTML = user.get('name');

ログインする

次に config.html の実装に移ります。ここでは名前を決めてもらい、それを匿名認証した後のユーザー名として使います。


確定ボタンを押した際のイベントは ons.getScriptPage().onInit に実装するのが基本です(onShowに実装するとイベントが何度も呼ばれるようになってしまいます)。


現在は以下のようになっています。ここは修正を加える必要はありません。

// この画面が表示された時、一度だけ呼ばれるイベント
ons.getScriptPage().onInit = function() {
  // すでにログインしている場合には名前を表示する
  show.bind(this)();
  // 確定ボタンを押した際の処理
  this.querySelector('#save').onclick = saveUser.bind(this);
}

show 関数は次のようになっています。ここも修正を加える必要はありません。

function show() {
  const user = ncmb.User.getCurrentUser()
  if (user) {
    this.querySelector('#name').value = user.get('name');
  }
}

確定ボタンを押した際の処理

確定ボタンを押した時のイベントは saveUser になります。コードは次のようになります。大事なのは入力された名前(name)を createUser に送っていることです。その結果が正常に終了したら、タブを一番初めの home.html に戻します。


// 確定ボタンを押した時の処理
async function saveUser(e) {
  try {
    e.preventDefault();
    const name = this.querySelector('#name').value;
    if (name && name.trim() === '') {
      ons.notification.alert('名前を入力してください');
      return;
    }
    await createUser(name);
    document.querySelector('#tabbar').setActiveTab(0);
  } catch (e) {
    console.log(e);
  }
}

createUser の処理は次のようになります。 ncmb.User.loginAsAnonymous() は匿名認証を行う処理です。この一行だけで匿名認証が完了します。


async function createUser(name) {
  // 匿名認証実行
  await ncmb.User.loginAsAnonymous();
  // ログインしているユーザ情報を取得
  const user = ncmb.User.getCurrentUser();
  // ACLを設定
  const acl = new ncmb.Acl;
  acl
    .setPublicReadAccess(true) // 全体読み込み可能
    .setUserWriteAccess(user, true); // ユーザ自身は書き込み可能
  // 更新
  await user
    .set('name', name)     // 入力された名前
    .set('working', false) // 勤務中のフラグ。デフォルトはfalse
    .set('acl', acl)       // ACL
    .update();             // 更新処理
}

匿名認証が完了すれば、 ncmb.User.getCurrentUser() でユーザー情報が取得できます。このユーザーはデフォルトで次のようなACLになっています。

  • ユーザー自身のみ読み書き可能

これでは、ほかの従業員から名前を見ることができませんので、ACLを次のように更新します。

  • 全体から読み込み可能(ユーザー自身含む)
  • ユーザー自身は書き込み可能

そして入力された名前を name として、さらに勤務中かどうかのフラグ working をデフォルト falseとして適用し、保存します。


データの保存と表示処理の実装について


home.htmlでの処理について

前回までで認証処理が終わって、home.htmlに戻ってくるところまで完了しました。今回は出勤(または退勤)ボタンを押して、それをログとしてmBaaSに記録する流れを作ります。


まず、home.htmlを初期化する処理を作成します。これはすでに記述してありますが、出退勤ボタンを押した際のイベントを設定しています。


// 初期化時(一回だけ)実行されます
ons.getScriptPage().onInit = async function(e) {
  // 出退勤ボタンを押した際のイベント処理です
  this.querySelector('#record').onclick = record.bind(this);
}

こちらもすでに記述されていますが、home.htmlを表示した際に毎回実行される処理です。

// この画面が表示される度に実行されます
ons.getScriptPage().onShow = async function(e) {
  const page = this;
  // 1. ログインしていなければ、名前を登録する画面に遷移します
  // ログインユーザを取得します(第1回目に記述した内容)
  const user = ncmb.User.getCurrentUser();
  if (!user) {
    // ログインしていないので config.html に遷移する
    document.querySelector('#tabbar').setActiveTab(2);
    return;
  }
  page.querySelector('#name').innerHTML = user.get('name');

  // 現在時刻を表示する処理
  callTimer(page);
  // 出退勤ボタンのラベル変更
  changeStatus(page);
  // 他の働いている人たちの一覧表示
  showWorkerList(page);
}

// タイマーで時間を更新します
function callTimer(page) {
  setInterval(() => {
    const d = new Date;
    page.querySelector('#time').innerHTML = d.toLocaleString();
  }, 1000);
}

この処理の中で行われている changeStatusshowWorkerList について後ほど確認します(callTimer は上記に記載済みです)。


出退勤ボタンを押した際のイベントを作成する

まず出退勤ボタンを押した際のイベント、recordについて記述します。7と8番の関数については前述の初期状態での処理と同じものです。


async function record(e) {
  try {
    e.preventDefault();

    // 1. 変数を準備する

    // 2. ACLを準備する

    // 3. データストアのデータを準備する

    // 4. 退勤の場合は勤務時間を記録する

    // 5. 保存する

    // 6. 出退勤状態を逆転する

    // 7. ステータスを変更する
    changeStatus(this);
    // 8. 勤務中の人リストを更新する
    showWorkerList(this);
  } catch (e) {
    console.log(e);
  }
}

1. 変数を準備する


必要な変数は次の通りです。

  • 現在時刻
  • ログインユーザ
  • 勤務中かどうかのフラグ

これらを次のように準備します。

// 現在時刻
const time = new Date;
// ログインユーザ
const user = ncmb.User.getCurrentUser();
// 勤務中かどうかのフラグ
const working = user.get('working');

2. ACLを準備する


ログインユーザの時と同じく、保存データに対するACLも設定します。今回は、ユーザ自身にも書き込み権限を付与しつつ、管理者ロール(グループ)からも読み書き可能とします。adminロールは管理画面で作成したものです。


const acl = new ncmb.Acl;
acl
  .setUserReadAccess(user, true)
  .setUserWriteAccess(user, true)
  .setRoleReadAccess('admin', true)
  .setRoleWriteAccess('admin', true);

3. データストアのデータを準備する


データストアは ncmb.DataStore('クラス名') で指定します。クラス名、は好きな名前を指定できます。データストアを new とすることで、インスタンス(データストアの行相当)が生成されます。


データストアのインスタンスについては set('カラム名', '値') の形式で自由にデータを設定できます。userはユーザオブジェクト、timeは時間、workingは真偽値になります。


const Record = ncmb.DataStore('Record');
const record = new Record;
record
  .set('user', user)
  .set('acl', acl)
  .set('time', time)
  .set('working', !working);

4. 退勤の場合は勤務時間を記録する


退勤する場合、勤務時間を記録しておきます。処理は下記のコードを参考にしてください。rangeはミリ秒単位の数字になります。出勤時には逆に現在時刻をユーザのデータとして入れておきます。


if (working) {
  // 働いていたら、勤務時間を記録する
  const range = time.getTime() - new Date(user.get('workingStartTime')).getTime();
  record
    .set('workingTime', range);
} else {
  // 仕事の開始時間を記録しておく
  user.set('workingStartTime', time);
}

5. 保存する


保存処理は save メソッドです。新規保存時は save 、更新時は update になります。これでデータストアにデータが送信されます。

await record.save();

6. 出退勤状態を逆転する


ユーザ自身は現在のステータスを出勤、退勤を逆転させます。こちらはすでにデータがありますので、update メソッドで更新します。

await user.set('working', !working).update();

7. ステータスを変更する


ステータスを更新する changeStatus 関数は初期表示の際にも行っています。ここでまとめて解説します。この処理はユーザの勤務状態を取得し、それを画面に反映しています。勤務状態は user.get('working') で取得できます。


changeStatus関数を下記のように変更してください。

function changeStatus(page) {
  // ログインユーザを取得し、その出退勤状態を判別します
  const user = ncmb.User.getCurrentUser();
  const working = user.get('working');
  page.querySelector('#record').innerHTML = `${working ? '退勤' : '出勤'}を記録する`;
}

8. 勤務中の人リストを更新する


勤務しているほかの人たちを取得する showWorkerList 関数についてです。これはユーザデータを検索するのですが、 ncmb.User の後に条件を追加して検索できます。


今回はイコール(=)条件である equalTo メソッドを使って、勤務中のフラグが立っている人たちを検索しています。その結果は #workers の中にOnsen UIのリストとして書き出しています。showWorkerList 関数を次のように書き直してください。


// 他の働いている人たちの一覧表示を行う関数
async function showWorkerList(page) {
  // ユーザデータを検索
  const workers = await ncmb.User
    .equalTo('working', true) // 勤務中フラグが立っている人のみ
    .limit(1000) // 最大1,000件としています
    .fetchAll();
  // 取得された結果を #workers に反映
  page.querySelector('#workers').innerHTML = workers
    .map(w => `<ons-list-item>${w.get('name')}さん</ons-list-item>`)
    .join('');
}

record関数について


record関数の内容は次のようになります。参考にしてください。

async function record(e) {
  try {
    e.preventDefault();

    // 1. 変数を準備する
    const time = new Date;
    const user = ncmb.User.getCurrentUser();
    const working = user.get('working');

    // 2. ACLを準備する
    const acl = new ncmb.Acl;
    acl
      .setUserReadAccess(user, true)
      .setUserWriteAccess(user, true)
      .setRoleReadAccess('admin', true)
      .setRoleWriteAccess('admin', true);

    // 3. データストアのデータを準備する
    const Record = ncmb.DataStore('Record');
    const record = new Record;
    record
      .set('user', user)
      .set('acl', acl)
      .set('time', time)
      .set('working', !working);

    // 4. 退勤の場合は勤務時間を記録する
    if (working) {
      // 働いていたら、勤務時間を記録する
      const range = time.getTime() - new Date(user.get('workingStartTime')).getTime();
      record
        .set('workingTime', range);
    } else {
      // 仕事の開始時間を記録しておく
      user.set('workingStartTime', time);
    }

    // 5. 保存する
    await record.save();
    // 6. 出退勤状態を逆転する
    await user.set('working', !working).update();
    // 7. ステータスを変更する
    changeStatus(page);
    // 8. 勤務中の人リストを更新する
    showWorkerList(page);
  } catch (e) {
    console.log(e);
  }
}

履歴の表示


ではhistory.htmlに移動します。この画面では Record クラスに保存されているデータを取得して、画面に表示します。ここでは、画面を表示した際に使うのがよさそうなので、 onShow イベントを使います。


最初、次のように書かれているはずです。 #records に書き出す部分はOnsen UIの処理なので、コードを参照してください。


ons.getScriptPage().onShow = async function(e) {
  // Recordクラスを検索


  // データを #records に書き出し
  // toTime関数は時間をフォーマットして書き出しているだけです。下記参照
  this.querySelector('#records').innerHTML = records
    .map(r => `<ons-list-item>
      <span class="list-item__title">
        ${toTime(r.get('workingTime'))}
      </span>
      <span class="list-item__subtitle">
        ${new Date(r.get('time').iso).toLocaleString()}より
      </span>
    </ons-list-item>`)
    .join('');
}

Recordクラスを検索


Recordクラスの検索は次のように書きます。ここで知って欲しいのは、誰のデータが欲しいのか指定していないことです。この状態で検索したとしても、自分のデータしか返ってきません。mBaaSのACL(アクセス制限)によって、誰が読み込みできるのか指定しています。


Recordクラスへの保存時に、自分とadminロールを持ったユーザーしか読み書きできないように指定したのを覚えているでしょうか。その結果、ほかのユーザーからは読み込みできないデータになっていますので、誰のデータという指定をしなくても問題なく運用できます。


下記処理を書き加えてください。勤務時間の履歴として表示されるはずです。

// Recordクラスを検索
const records = await ncmb.DataStore('Record')
  .limit(100)                  // 最大100件
  .order('createDate', true)   // 新しいものを上に
  .equalTo('working', false)   // 勤務中フラグが立っていないものを対象
  .exists('workingTime')       // workingTimeが入力されているもの
  .fetchAll();                 // 全データを取得

バックアップの目的


業務システムではデータを締めて更新できないようにするということがよく行われます。経理や人事などバックオフィス系でよく行われる処理です。これによって過去データが改ざんできないようにしたり、保全が可能になります。


今回はそこまで厳しくありませんが、勤務データをデータストアからファイルストアへ移しておくことで、いつでも参照できるデータとして役立てられるようになるでしょう。


スクリプト機能を利用する


データストアからファイルストアへデータをコピーする処理を、mBaaSのスクリプト処理で実現します。mBaaSにはタイマー機能はありませんが、Google Apps Scriptや社内のサーバなどから定期的に呼び出せば、自動的にバックアップが作成される仕組みです。


まず csv.js というファイルを作成します。初期の内容は次のスライドのようにします。これはスクリプト機能の基本形になります。


const NCMB = require('ncmb');

module.exports = async function(req, res) {
  try {
    // 1. mBaaSの初期化処理

     // 2. 管理者ユーザーでログイン

    // 3. 変数の準備
    // 処理対象の日付
    const d = req.query.date ? new Date(req.query.date) : new Date;
    // 処理対象の日付の00:00を取得
    const startDate = new Date(`${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()} 00:00:00 UTC+0900`);
    // 処理対象の日付の次の日(00:00)を取得
    const endDate = new Date(`${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate() + 1} 00:00:00 UTC+0900`);
    // 保存するファイル名を、日付を元に設定
    const fileName = `working-${startDate.getFullYear()}-${startDate.getMonth() + 1}-${startDate.getDate()}.csv`;
    d.setDate(d.getDate() + 1);

    // 4. データストアの検索

    // 5. CSV化

    // 6. ファイルストアへの保存

    // 7. 処理完了
    res.send('Uploaded');
  } catch (err) {
    // エラー
    res.send(JSON.stringify(err));
  }
}

1. mBaaSの初期化処理


Monacaアプリと同じように、まず最初にmBaaSを初期化します。アプリケーションキーとクライアントキーを書き換えてください。

const applicationKey = 'YOUR_APPLICATION_KEY';
const clientKey = 'YOUR_CLIENT_KEY';
const ncmb = new NCMB(applicationKey, clientKey);

2. 管理者ユーザーでログイン


管理ユーザは第1回目の記事で作成してもらったユーザになります。ユーザ名とパスワードをそれぞれ書き換えてください。

await ncmb.User.login('ADMIN_USER_NAME', 'ADMIN_PASSWORD');

3. 変数の準備


今回必要な変数は次の4つになります。それぞれ日付(d)を基本としています。dはクエリパラメータのdateで変更できます(デフォルトは今日の日付です)。こちらは記述済みです。


- d  
処理対象の日付
- startDate  
処理対象の日付の00:00
- endDate  
処理対象の日付の次の日00:00
- fileName  
保存するファイル名日付を元に設定

4. データストアの検索


データストアのクラス、Recordを検索します。 include を使うと、別なデータストアのクラスデータ(ポインタ)を一緒に取得できます。そして対象データを createDate の処理対象日以上と、処理対象日の次の日未満の2つで絞り込みます。


const ary = await ncmb.DataStore('Record')
  // ユーザデータも一緒に取得します
  .include('user')
  // 勤務時間が入っているものだけ
  .equalTo('working', false)
  // 処理対象日時(00:00)以上
  .greaterThanOrEqualTo('createDate', startDate)
  // 処理対象日時の次の日(00:00)未満
  .lessThan('createDate', endDate)
  // 最大1000件(mBaaSの最大取得件数)
  .limit(1000)
  .fetchAll();

5. CSV化


取得したデータをCSV化します。そのために、必要なデータを配列に入れていきます。mBaaSではデータの取得は get メソッドを使います。また、日付は直接日付データが入っていません。 {"_type": "Date", "iso": (日付)} という形になっていますので注意してください。


const csv = [];
const columns = ['objectId', 'time', 'name', 'startAt', 'endAt'];
csv.push(columns.map(c => `"${c}"`).join("\t"));
for (let row of ary) {
  const params = [
    row.get('objectId'),
    row.get('time').iso,
    Object.keys(row.get('user')).length > 0 ? row.get('user').name.replace(/"/g, '""') : '',
    new Date(row.get('time').iso).toLocaleString(),
    new Date(new Date(row.get('time').iso).getTime() + row.get('workingTime')).toLocaleString()
  ];
  csv.push('"' + params.join("\"\t\"") + '"');
}

6. ファイルストアへの保存


CSVのデータができあがったら、ファイルストアに書き込みます。Bufferを使って文字列をBufferにしたら、それをファイル名とともに File.upload メソッドに適用します。これでファイルストアへのアップロード処理が完了です。


// 6. ファイルストアへの保存
const buf = Buffer.from(csv.join("\r\n"), 'UTF-8');
await ncmb.File.upload(fileName, buf);

完成版はMonacaプロジェクトの直下に csv.js として保存してあります。


スクリプトをアップロードする


csv.js ができあがったら、ファイルを管理画面からアップロードしましょう。今回は次のように指定してください。

  • メソッド
    GET
  • ファイルの状態
    実行可能

ほかは元のままで大丈夫です。そしてアップロードするボタンを押します。しばらく(1分程度)待つと、実行可能になります。


テストで実行する


では管理画面で実行してみます。スクリプトの一覧からcsv.jsを選んでください。

実行タブを選択して、Queryを date=2020-09-10 とします。日付は今日の日付にしてください。


実行ボタンを押すと確認が出ますので、実行するボタンを押します。

処理がうまくいけば、ファイルストアに working-2020-09-10.csv といったCSVファイルが保存されているはずです。


定期実行について

もし自社内にサーバがあれば、次のようなコードでスクリプトを実行できます。これは Node.js で書いていますので、サーバ上にNode.jsやNCMBのJavaScript SDKが必要です。今回のハンズオンでは対象外としています。


const NCMB = require('ncmb');

const applicationKey = 'YOUR_APPLICATION_KEY';
const clientKey = 'YOUR_CLIENT_KEY';
const ncmb = new NCMB(applicationKey, clientKey);

(async () => {
  const res = await ncmb.Script
    .query({date: '2020-09-10'}) // 日付は適当なものを指定してください
    .exec('GET', 'csv.js');
  console.log(res.body);
  // 処理がうまくいけば uploaded と出力されます
})();

定期実行については Google Apps Scriptのタイマートリガーと、Google Apps Script用のSDKを組み合わせても実現できます。

Google Apps Scriptのトリガーを使ってスクリプトを自動テストする


まとめ


今回のハンズオンでは次の4つの機能を利用しました。

  1. 会員管理(匿名認証)
  2. データストア(データの保存、検索)
  3. ファイルストア(アップロード)
  4. スクリプト
1
3
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
1
3