Firebaseを利用した、ほぼ１日クオリティー(実装)の勤務時間管理システム

#概要
ほぼ１日で実装した、勤務時間管理システムを作成したはなし。
※ソースの作成が1日であり、設計とか調査は何日か取っています。
※何点か不満はありますが、運用はできます。

会社で使っている勤怠管理システムが、プロジェクトコードがないと入力しずらい(後から変更するのが面倒)なので、
なんか時間だけを管理できる仕組みがあればいいなと思い作成した。

## 利用したフレームワーク等
1. FirebaseHosting(サイト公開用)
1. FirebaseAuth(ログイン関係)
1. FirebaseCloudFirestore(データー保存関連)
1. bootstrap4(画面デザイン)
1. SweetAlert2(ポップアップメッセージ)
2. moment.js (jsの時間に関するライブラリー)

## 作成した画面
* ログイン画面(firebaseのものを利用)
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3236393735362f38663439663735342d613066642d313633322d303135322d6131636334643030653966382e706e67.png](https://qiita-image-store.s3.amazonaws.com/0/269756/762cfb87-fd7e-bff1-0ea1-46778984b19c.png)
* 入力と一覧画面
![image.png](https://qiita-image-store.s3.amazonaws.com/0/269756/d96c392c-c183-002f-8fc0-8ab3961ad613.png)


## 主な動き
* 出社のボタンを押下したら、出社として現在の時間を記録する。
* 退社のボタンを押下したら、退社として現在の時間を記録する。
* 一覧に記録したデータを出力する。

## ソース
ログインについては、下記記事で書いているので割愛
[Firebaseでログインを導入したら驚くほど簡単に出来た](https://qiita.com/hirotann618/items/285c3bb373703663dc9a)

### ソースディレクトリ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/269756/407c42ee-b4b3-abef-1258-befd2a2a0495.png)
「404.html」はfirebaseが自動生成
「index.html/index.js」はログインで使用
「main.html/control.js」はシングルページアプリケーションとして、作ろうとした名残り今回１ページしかないから意味がない。

### ソースコード

* 画面のテンプレートのソース

``` html:main.html

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Kintai</title>

  <!-- Firebase App is always required and must be first -->
  <script src="https://www.gstatic.com/firebasejs/5.5.2/firebase-app.js"></script>

  <!-- Add additional services that you want to use -->
  <script src="https://www.gstatic.com/firebasejs/5.5.2/firebase-auth.js"></script>
  <script src="https://www.gstatic.com/firebasejs/5.5.2/firebase-database.js"></script>
  <script src="https://www.gstatic.com/firebasejs/5.5.2/firebase-firestore.js"></script>
  <script src="https://www.gstatic.com/firebasejs/5.5.2/firebase-messaging.js"></script>
  <script src="https://www.gstatic.com/firebasejs/5.5.2/firebase-functions.js"></script>

  <script src="https://cdn.firebase.com/libs/firebaseui/3.1.1/firebaseui.js"></script>
  <link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/3.1.1/firebaseui.css" />

  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
    integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

  <!-- sweetalert2 -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/7.33.1/sweetalert2.min.css">

  <script src="./js/moment.min.js"></script>

</head>

<body>
  <header id="header">
  </header>

  <main id="main">
  </main>

  <footer id="footer">
  </footer>
  <!-- Optional JavaScript -->

  <!-- jQuery first, then Popper.js, then Bootstrap JS -->
  <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
    integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
    crossorigin="anonymous"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
    integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
    crossorigin="anonymous"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
    integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
    crossorigin="anonymous"></script>

  <script src="https://www.gstatic.com/firebasejs/5.8.3/firebase.js"></script>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/7.33.1/sweetalert2.all.min.js"></script>

  <script type="text/javascript" src="./js/control.js"></script>
  <script type="text/javascript" src="./js/main.js"></script>
</body>

</html>

```

* 画面の遷移をコントロールするJS(意味がない)

``` js:js/control.js

class Control{

    constructor(){

        jQuery.ajax({
            url: '/header.html',
            type:'Get',
        })
        .done( (data) => {
            jQuery('#header').html(data);
        })

        if(('localStorage' in window) && (window.localStorage !== null)) {
            // ローカルストレージが使える
        } else {
            alert("ローカルストレージが利用できません。");
        }

        this.pageurl = window.localStorage.getItem('pageurl');

        if(typeof this.pageurl === "undefined"){
            this.pagechange(this.pageurl)
        }else{
            this.pagechange('/dashboard.html')
        }
    }

    pagechange(url){

        window.localStorage.setItem('pageurl',url);
        jQuery.ajax({
            url: url,
            type:'Get',
        })
        .done( (data) => {
            jQuery('#main').html(data);
        })
    } 
}

var contol = new Control(); 
```

* ヘッダーの画面ソース

``` html:header.html
<nav class="navbar navbar-expand navbar-dark bg-dark">
    <h5 class="navbar-brand">Kintai</h5>
</nav>
```

* 今回のメインページとなる画面ソース
ボタン2つとテーブルがあるだけ

``` html:dashboard.html
<div class="container">
    <div class="card-deck mb-3 text-center">
        <div class="card box-shadow">
            <div class="card-header">
                <h2>勤怠記録</h2>
            </div>
            <div class="card-body">
                <button type="button" class="btn btn-primary btn-block btn-lg" onclick="main.onClickin(main.useremail);">出社</button>
                <button type="button" class="btn btn-primary btn-block btn-lg" onclick="main.onClickout(main.useremail);">退社</button>

                <table class="table table-striped">
                    <thead>
                        <tr>
                            <th>日付</th>
                            <th>出社</th>
                            <th>退社</th>
                        </tr>
                    </thead>
                    <tbody id="table">
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</div>
```
* ボタンの動きや一覧を表示するためのjs　(後ほど解説)

``` js:js/main.js
class Main {

    constructor() {
        this.email = null;
    }

    get useremail() {
        return this.email
    }

    set useremail(value) {
        this.email = value;
    }

    static init(obj) {
        var config = {
            apiKey: "",
            authDomain: "",
            databaseURL: "",
            projectId: "",
            storageBucket: "",
            messagingSenderId: ""
        };
        firebase.initializeApp(config);

        //オブザーバーを利用したログインを行った場合の
        firebase.auth().onAuthStateChanged(function (user) {
            if (user) {
                // User is signed in.

                obj.useremail = user.email;

                var db = firebase.firestore();
                db.collection(user.email).get().then(function (querySnapshot) {
                    querySnapshot.forEach(function (doc) {
                        // doc.data() is never undefined for query doc snapshots
                        console.log(doc.id, " => ", doc.data());
                        jQuery('#table').append('<tr>');
                        jQuery('#table').append('<td>' + moment(doc.id, "YYYYMMDD").format('YYYY/MM/DD') + '</td>');
                        jQuery('#table').append('<td>' + doc.data().start + '</td>');
                        jQuery('#table').append('<td>' + doc.data().end + '</td>');
                        jQuery('#table').append('</tr>');
                    });
                });

            } else {
                window.location.href = 'index.html'
            }
        });
    }

    onClickin(useremail) {
        var db = firebase.firestore();

        db.collection(useremail).doc(moment().format('YYYYMMDD')).set({
            start: moment().format('HH:mm:ss')
        }, { merge: true })
            .then(function () {
                Swal.fire({
                    type: "success",
                    title: "出社を記録しました。",
                });
                db.collection(useremail).get().then(function (querySnapshot) {
                    querySnapshot.forEach(function (doc) {
                        // doc.data() is never undefined for query doc snapshots
                        console.log(doc.id, " => ", doc.data());
                        jQuery('#table').empty();
                        jQuery('#table').append('<tr>');
                        jQuery('#table').append('<td>' + moment(doc.id, "YYYYMMDD").format('YYYY/MM/DD') + '</td>');
                        jQuery('#table').append('<td>' + doc.data().start + '</td>');
                        jQuery('#table').append('<td>' + doc.data().end + '</td>');
                        jQuery('#table').append('</tr>');
                    });
                });
            })
            .catch(function (error) {
                Swal.fire({
                    type: "error",
                    title: "エラーが発生しました。",
                    text: error,
                });
            });
    }

    onClickout(useremail) {
        var db = firebase.firestore();

        db.collection(useremail).doc(moment().format('YYYYMMDD')).set({
            end: moment().format('HH:mm:ss')
        }, { merge: true })
            .then(function () {
                Swal.fire({
                    type: "success",
                    title: "退社を記録しました。",
                });
                db.collection(useremail).get().then(function (querySnapshot) {
                    querySnapshot.forEach(function (doc) {
                        // doc.data() is never undefined for query doc snapshots
                        console.log(doc.id, " => ", doc.data());
                        jQuery('#table').empty();
                        jQuery('#table').append('<tr>');
                        jQuery('#table').append('<td>' + moment(doc.id, "YYYYMMDD").format('YYYY/MM/DD') + '</td>');
                        jQuery('#table').append('<td>' + doc.data().start + '</td>');
                        jQuery('#table').append('<td>' + doc.data().end + '</td>');
                        jQuery('#table').append('</tr>');
                    });
                });
            })
            .catch(function (error) {
                Swal.fire({
                    type: "error",
                    title: "エラーが発生しました。",
                    text: error,
                });
            });
    }
}
var main = new Main();
Main.init(main);

```

## 動きについて解説
##### 画面表示のとき
1. ログインの状態確認
2. データベースにアクセス
3. 取得したデータをもとにテーブルを作成

ログイン状況の確認と、データベースよりデータの取得を行っています。
ログイン状況は共通にさせるべきだったと思っています。


``` js
       //1. ログインの状態確認
        firebase.auth().onAuthStateChanged(function (user) {
            if (user) { // ログイン確認した場合
                
                obj.useremail = user.email;

                //2. データベースにアクセス
                //データアクセスオブジェクトを作成
                var db = firebase.firestore();
                //データベースにデータを取得する。
                db.collection(user.email).get().then(function (querySnapshot) {
                    //3. 取得したデータをもとにテーブルを作成
                    querySnapshot.forEach(function (doc) {
                        console.log(doc.id, " => ", doc.data());
                        jQuery('#table').append('<tr>');
                        jQuery('#table').append('<td>' + moment(doc.id, "YYYYMMDD").format('YYYY/MM/DD') + '</td>');
                        jQuery('#table').append('<td>' + doc.data().start + '</td>');
                        jQuery('#table').append('<td>' + doc.data().end + '</td>');
                        jQuery('#table').append('</tr>');
                    });
                });

            } else {
                //ログインユーザーが確認できない場合はログイン画面へ
                window.location.href = 'index.html'
            }
        });
```

#### 出社/退社押下時
1.　データをデータベースに設定する。
2.　成功時は、ポップアップ表示して、一覧を表示する。
3.　失敗のときは、 ポップアップ表示する。  　

```js
    //出社のとき
    onClickin(useremail) {
        //データアクセスオブジェクトを作成
        var db = firebase.firestore();
        
        //データベースにデータを設定する。
        db.collection(useremail).doc(moment().format('YYYYMMDD')).set({
            start: moment().format('HH:mm:ss')
        },
        //データをマージするように設定 
        { merge: true })
           //設定成功時
            .then(function () {
                //ポップアップ表示
                Swal.fire({
                    type: "success",
                    title: "出社を記録しました。",
                });
                
                //一覧の更新
                db.collection(useremail).get().then(function (querySnapshot) {
                    querySnapshot.forEach(function (doc) {
                        // doc.data() is never undefined for query doc snapshots
                        console.log(doc.id, " => ", doc.data());
                        jQuery('#table').empty();
                        jQuery('#table').append('<tr>');
                        jQuery('#table').append('<td>' + moment(doc.id, "YYYYMMDD").format('YYYY/MM/DD') + '</td>');
                        jQuery('#table').append('<td>' + doc.data().start + '</td>');
                        jQuery('#table').append('<td>' + doc.data().end + '</td>');
                        jQuery('#table').append('</tr>');
                    });
                });
            })
            .catch(function (error) {
                Swal.fire({
                    type: "error",
                    title: "エラーが発生しました。",
                    text: error,
                });
            });
    }

    //退社のとき
    onClickout(useremail) {
        var db = firebase.firestore();

        db.collection(useremail).doc(moment().format('YYYYMMDD')).set({
            end: moment().format('HH:mm:ss')
        }, { merge: true })
            .then(function () {
                Swal.fire({
                    type: "success",
                    title: "退社を記録しました。",
                });
                db.collection(useremail).get().then(function (querySnapshot) {
                    querySnapshot.forEach(function (doc) {
                        // doc.data() is never undefined for query doc snapshots
                        console.log(doc.id, " => ", doc.data());
                        jQuery('#table').empty();
                        jQuery('#table').append('<tr>');
                        jQuery('#table').append('<td>' + moment(doc.id, "YYYYMMDD").format('YYYY/MM/DD') + '</td>');
                        jQuery('#table').append('<td>' + doc.data().start + '</td>');
                        jQuery('#table').append('<td>' + doc.data().end + '</td>');
                        jQuery('#table').append('</tr>');
                    });
                });
            })
            .catch(function (error) {
                Swal.fire({
                    type: "error",
                    title: "エラーが発生しました。",
                    text: error,
                });
            });
    }
```

# 作ってみて/今後について
お休みのとき１日を利用して実装してみました。それなりに楽しかったです。
FirebaseCloudFunctionsについては、いろいろと考えどころがあります。
ローカルに一度落としてローカルのindexedDBと同期しながら使うのを考えると面白いかなと思いました。

とりあえず完成した感じでとても使いにくい部分があるので、少しずつ改良を行っていきたいと思います。
今退社と出社が変になるので、とりあえずは出社したら退社するとかあとは、編集ですね。

ここまで読んでいただきありがとうございます。