何って……自分用の勤怠管理システムを作っただけだが？

異世界に転生したので、自分用の勤怠管理システム「[kintai](https://kintai.munieru.app/)」を作りました。

## あらすじ
僕が11月から働いている会社では、ありがたいことに[フレックスタイム制度](https://ja.wikipedia.org/wiki/%E3%83%95%E3%83%AC%E3%83%83%E3%82%AF%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%A0%E5%88%B6)が導入されています。
ところが出退勤の記録は物理的な[タイムレコーダー](https://ja.wikipedia.org/wiki/%E3%82%BF%E3%82%A4%E3%83%A0%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%80%E3%83%BC)で行なうため、日々の業務時間の把握がしづらいという問題がありました。
そこで、タイムカードに記録された時間をもとに業務時間の計算を行なうウェブアプリケーションを作ることにしたのです。

## 特徴
![https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132608/aae32849-63ae-2c71-5c7e-0ea95476dc00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132608/aae32849-63ae-2c71-5c7e-0ea95476dc00.png)

滞在時間を入力すると、自動的に業務時間が計算されます。
データはブラウザに保存されるため、再度アクセスすれば前回の続きから入力することができます。
勤怠管理システムを名乗ってはいるものの、現時点でできることはそれだけです。
ええ、タイトルは若干釣りです。

## 使用技術
今回使用した主な技術は以下の通りです。

- [Nuxt.js](https://nuxtjs.org/)：JavaScriptフレームワーク
- [Vuetify.js](http://vuetifyjs.com/)：UIフレームワーク
- [Netlify](https://www.netlify.com/)：静的ウェブサイトホスティングサービス

また、日時を扱うためのライブラリを3つ使っています。

- [Luxon](https://moment.github.io/luxon/)：Dateオブジェクトのラッパーライブラリ
- [date-fns](https://date-fns.org/)：日時のユーティリティライブラリ
- [@holiday-jp/holiday_jp](https://www.npmjs.com/package/@holiday-jp/holiday_jp)：日本の祝日ライブラリ

できれば日時の操作はdate-fns一本に絞りたかったのですが、肝心のDuration（時間量）を扱う関数がなかったので、やむなくLuxonも導入しました。

## こだわりポイント
### リアルタイム計算
Excelのようにリアルタイムに計算をするために、Vue.jsの算出プロパティや双方向データバインディングなどの仕組みをおおいに活用しています。
計算ロジックは、次の通りです。

- 滞在時間：ユーザーが入力
- 休憩時間：ユーザーが入力
- 実労働時間：滞在時間 - 休憩時間
- 労働時間の単位：ユーザーが入力
- 労働時間：実労働時間を労働時間の単位で切り捨て
- 所定労働時間：ユーザーが入力
- 残業時間：所定労働時間 - 労働時間

### それなりに汎用性がある
僕の会社では15分単位で労働時間が計算されますが、5分単位や1分単位の会社もあるでしょう。
また、場合によっては時短勤務で7時間労働という人もいるかもしれません。
そのように多種多様なケースに対応するため、計算に使用する値（休憩時間、所定労働時間、労働時間の単位）を設定でカスタマイズできるようにしました。

![https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132608/2af26704-d54a-d02d-727b-252692b782a7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132608/2af26704-d54a-d02d-727b-252692b782a7.png)

これらの値はあくまでもデフォルト値なので、日毎に変更することも可能です。

### データをブラウザに保存
データの保存先として、[IndexedDB](https://developer.mozilla.org/ja/docs/Web/API/IndexedDB_API)を使用しています。
IndexedDBのラッパーライブラリはいくつかあるようですが、今回は自分でラッパークラスを作りました。

```js:modules/database.js
const DB_NAME = 'kintai'
const DB_VERSION = 1

class Database {
  constructor (indexedDB) {
    this.indexedDB = indexedDB
  }

  initialize () {
    return new Promise((resolve, reject) => {
      const request = this.indexedDB.open(DB_NAME, DB_VERSION)
      request.onupgradeneeded = (event) => {
        const db = event.target.result
        const timeStore = db.createObjectStore('times', { keyPath: 'date' })
        timeStore.createIndex('date', 'date', { unique: true })
      }
      request.onsuccess = (event) => {
        const db = event.target.result
        this.db = db
        resolve()
      }
      request.onerror = (event) => {
        reject(event)
      }
    })
  }

  getRecordsByRange (table, { key, lower, upper }) {
    return new Promise((resolve, reject) => {
      const tx = this.db.transaction(table, 'readonly')
      const store = tx.objectStore(table)
      const index = store.index(key)
      const range = IDBKeyRange.bound(lower, upper)
      const request = index.openCursor(range)
      const records = []
      request.onsuccess = (event) => {
        const cursor = event.target.result
        if (cursor) {
          const record = cursor.value
          records.push(record)
          cursor.continue()
        } else {
          resolve(records)
        }
      }
      request.onerror = (event) => {
        reject(event)
      }
    })
  }

  putRecord (table, record) {
    return new Promise((resolve, reject) => {
      const tx = this.db.transaction(table, 'readwrite')
      const store = tx.objectStore(table)
      const request = store.put(record)
      request.onsuccess = (event) => {
        resolve()
      }
      request.onerror = (event) => {
        reject(event)
      }
    })
  }
}

export default Database
```

初めてIndexedDBを使いましたが、JavaScriptのオブジェクトをそのまま保存できる（JSON文字列に変換する必要がない）のと、その名の通りインデックスをつけることができるのが便利でした。
しかしながら見ての通り若干APIがつらい感じなので、本格的に使う場合はラッパーライブラリの導入をおすすめします。

## クソポイント
### 自分で値を入力する必要がある
当たり前といえば当たり前ですが、タイムカードに記録された時間を自分で入力する必要があります。

### iPhoneで動作しない
iPhoneで見るとテーブルを横にスクロールしないと残業時間が見れないのですが、それ以前にテーブル内のフォームに数値を入力できないという致命的な問題があります。
設定画面のフォームは正常に動作するので、Vuetifyのコンポーネントの中で素のinput要素を使っているせいかもしれません。

### 使い方がわかりづらいかもしれない
製作者である僕自身は当然使い方が分かりますが、他の人が見たとき分かるかどうかが微妙なところです。
ヘルプページを設けて、用語の定義も含めて使い方の説明を載せるべきかもしれません。

## ソースコード
kintaiのソースコードは、GitHubで公開しています。

* [munierujp/kintai](https://github.com/munierujp/kintai)