「#ダイエット管理」タグアーカイブ

【LARAVEL】【ダイエット支援】VPSにデプロイする

前回までの状況はこちら。

最低限必要な機能は出来上がったので、本番サーバであるVPSで稼働させます。

gitからソースファイルをクローン

git clone https://github.com/takishita2nd/diet-mng.git

パーミッション変更

chmod -R 777 storage/
chmod 777 bootstrap/cache/

.envを作成し、データベースの設定と、URLの設定を記入。

cp .env.example .env
vi .env

データベースにログインし、データベースを作成。

mysql> create database diet_mng;

composerを使ってLaravelの環境を構築

sudo apt-get install composer
composer install

.envにkyeを生成

php artisan key:generate

npmでVue.jsを使用できるようにする

sudo apt-get install npm
sudo apt-get install libpng-dev
npm install
npm run prod

データベース構築(マイグレート)

php artisan migrate

nginxの設定

URLでブログとダイエット管理を分けようと思ったのですが、上手く設定できなかったので、ポート番号で分けます。

cd /etc/nginx/sites-enabled
sudo cp default laravel
sudo vi laravel
server {
        listen 8443 ssl default_server;
        listen [::]:8443 ssl default_server;
        ssl_certificate     /etc/letsencrypt/live/taki-lab.site/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/taki-lab.site/privkey.pem;

        root /var/www/html/diet-mng/public;

        index index.php index.html index.htm index.nginx-debian.html;

        server_name taki-lab.site;

        location / {
                try_files $uri $uri/ /index.php?$query_string;
        }

        location ~ \.php$ {
                fastcgi_pass   unix:/run/php/php7.2-fpm.sock;
                fastcgi_index  index.php;
                fastcgi_param  SCRIPT_FILENAME  $document_root/index.php;
                include        fastcgi_params;
        }
}

ポート番号は8443を使用しました。

ブログ用の設定では動きませんので、いろんなサイトを調べた結果、このような設定で動作できます。

rootはプロジェクトディレクトリ/publicを指定します。publicの下のindex.phpにアクセスするように指定します。

nginxの設定と読み込み

sudo systemctl reload nginx.service

何も表示されなければ書式は合っています。

エラーが出たら/var/log/nginx/error.logを確認してください。

ここまで上手くいけばトップページが表示されるはず。

トップページ書き換えるの忘れてた。

ログインしてデータ入力できることを確認する。

以下、詰まったところ

modelに以下を記入しないとデータベースクエリが動かなかった。

protected $table = 'weight_managements';

https://qiita.com/igz0/items/d14fdff610dccadb169e

テーブル名を明示的に指定しないといけないらしい。

ここらへん、ローカル環境にフィードバックさせます。

というわけで、

ダイエット管理サイト、以下からアクセスできますので、よかったら使ってみてください。

https://taki-lab.site:8443/

【LARAVEL】【ダイエット支援】グラフ表示用のデータを取得するその2

前回までの状況はこちら。

最新ソースはこちら(gitHub)

https://github.com/takishita2nd/diet-mng

前回はAPI側を作成したので、今回はフロント側を作成します。

使用したライブラリは、chart.jsです。

なんか、Vue.js用に拡張されたvue-chart.jsというものもあるみたいなのですが、うまく動かなかったので、chart.jsにしました。

原因はわからん。

npm install chart.js --save

でインストール。

resouces/assets/app.jsに以下を書き込みます。

require('chart.js');

これでどこでもchart.jsが使えます。

<template>
    <div>
        <div class="dashboard">
            <div class="chart">
                <canvas id="weight"></canvas>
            </div>
            <div class="command">
                <ul>
                    <li><a @click="onClickInput">クイック入力</a></li>
                    <li><a href="/weight">詳細</a></li>
                </ul>
            </div>
        </div>
        <weight-input-dialog-component :show="showInputDialogContent" @update="invokeUpdateList"></weight-input-dialog-component>
    </div>
</template>

<script>
export default {
    data() {
        return {
            showInputDialogContent: false,
            datetimeList: [],
            weightList: [],
            fat_rateList: [],
            bmiList: [],
        };
    },
    created: function() {
    },
    mounted: function() {
        this.graphUpdate();
    },
    methods: {
        onClickInput: function() {
            this.showInputDialogContent = true;
        },
        invokeUpdateList: function() {
            this.graphUpdate();
        },
        graphUpdate: function() {
            this.datetimeList = [];
            this.weightList = [];
            this.fat_rateList = [];
            this.bmiList = [];
            var ctx = document.getElementById("weight");
            var self = this;
            axios.post('api/weight/graph').then(function(response){
                response.data.datas.forEach(element => {
                    self.datetimeList.push(element.datetime);
                    self.weightList.push(element.weight);
                    self.fat_rateList.push(element.fat_rate);
                    self.bmiList.push(element.bmi);
                });
                var myChart = new Chart(ctx, {
                    type: 'line',
                    data: {
                        labels: self.datetimeList,
                        datasets: [
                            {
                                yAxisID: 'weight',
                                label: '体重(kg)',
                                data: self.weightList,
                                borderColor: "rgba(255,0,0,1)",
                                backgroundColor: "rgba(0,0,0,0)"
                            },
                            {
                                yAxisID: 'fat_rate',
                                label: '体脂肪率(%)',
                                data: self.fat_rateList,
                                borderColor: "rgba(0,255,0,1)",
                                backgroundColor: "rgba(0,0,0,0)"
                            },
                            {
                                yAxisID: 'bmi',
                                label: 'BMI',
                                data: self.bmiList,
                                borderColor: "rgba(0,0,255,1)",
                                backgroundColor: "rgba(0,0,0,0)"
                            },
                        ]
                    },
                    options: {
                        title: {
                            display: true,
                            text: '最近の記録',
                        },
                        elements: {
                            line: {
                                tension: 0,
                            }
                        },
                        scales: {
                            yAxes: [
                                {
                                    id: 'weight',
                                    type: 'linear',
                                    position: 'left',
                                    ticks: {
                                        suggestedMax: 100,
                                        suggestedMin: 0,
                                        stepsize: 10,
                                        callback: function(value, index, values){
                                            return value + 'kg';
                                        }
                                    }
                                },
                                {
                                    id: 'fat_rate',
                                    type: 'linear',
                                    position: 'right',
                                    ticks: {
                                        suggestedMax: 100,
                                        suggestedMin: 0,
                                        stepsize: 10,
                                        callback: function(value, index, values){
                                            return value + '%';
                                        }
                                    }
                                },
                                {
                                    id: 'bmi',
                                    type: 'linear',
                                    position: 'left',
                                    ticks: {
                                        suggestedMax: 100,
                                        suggestedMin: 0,
                                        stepsize: 10,
                                        callback: function(value, index, values){
                                            return value;
                                        }
                                    }
                                },
                            ]
                        }
                    }
                });
            }).catch(function(error){
            });
        }
    }
}
</script>

var ctx = document.getElementById(“weight”);でcanvasの要素を取得し、var myChart = new Chart(ctx, {…})で{…}の部分にデータを入れることで、canvasにグラフが描画されます。

axiosのpostメソッドを使用して、前回作成したAPIを呼び出し、表示するデータを項目ごとにリスト化してchart.jsに渡します。

動作結果はこんな感じ。

思ったよりいい感じです。

【LARAVEL】【ダイエット支援】グラフ表示用のデータを取得する

前回までの状況はこちら。

最新ソースはこちら(gitHub)

https://github.com/takishita2nd/diet-mng

これからダッシュボードに表示するグラフを作成するんですが、それに使用するデータの取得を行います。

今回グラフに表示するのは、最近10日間のデータのみ、という想定をしています。

    public function getGraphData($user)
    {
        $datetimes = [];
        for($i = 0; $i < 10 ; $i++) {
            $datetimes[] = date('Y-m-d', strtotime('today - '.$i.' day'));
        }

        return $user->WeightManagements()
                    ->whereIn(DB::raw('date_format(datetime, "%Y-%m-%d")'), $datetimes)
                    ->get();
    }

今回のポイントは特定ユーザに関連するデータのみを対象にする場合、

$user->WeightManagements()->whereIn()

という感じで、リレーションからのSQLビルダ作成、というやり方ができるらしい。

前回のホテル予約管理ではjoinやらwhereやら使いまくっててコードがごちゃごちゃしていたけど、その必要はなかった。

ただし、ここからgroupbyやら統計関連の関数を使用するとうまく行かない。

どうやら、laravelさんがget()の出力内容にuser_idなどの情報を含めているためにグループ化ができないため、SQL実行エラーとなるようです。

なので、一日の最新を取り出す、ということになると、このやり方ではSQLビルダは使用できなくって、PHP側で処理することになります。

今回はめんどくさいので、最新10日のデータ全部、としました。

きちんとtinkerで動作確認したよ。

    /**
     * グラフ用データを取得する
     */
    public function graph(Request $request)
    {
        return response()->json(['datas' => $this->weightManagement->getGraphData(Auth::user())]);
    }
Route::post('api/weight/graph', 'Weight\ApiController@graph');
>>> $rep->getGraphData($user)
=> Illuminate\Database\Eloquent\Collection {#3849
     all: [
       App\Model\WeightManagement {#3843
         id: 1,
         datetime: "2020-05-28 02:14:00",
         weight: "80.00",
         fat_rate: "17.00",
         bmi: "27.00",
         created_at: "2020-05-28 02:14:50",
         updated_at: "2020-05-28 02:14:50",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#3840
           user_id: 1,
           weight_management_id: 1,
         },
       },
       App\Model\WeightManagement {#3844
         id: 2,
         datetime: "2020-05-28 02:15:00",
         weight: "80.00",
         fat_rate: "17.00",
         bmi: "27.00",
         created_at: "2020-05-28 02:15:10",
         updated_at: "2020-05-28 02:15:10",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#3841
           user_id: 1,
           weight_management_id: 2,
         },
       },
       App\Model\WeightManagement {#3845
         id: 5,
         datetime: "2020-05-28 02:34:00",
         weight: "80.00",
         fat_rate: "17.00",
         bmi: "28.00",
         created_at: "2020-05-28 02:34:49",
         updated_at: "2020-06-01 01:20:56",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#3779
           user_id: 1,
           weight_management_id: 5,
         },
       },
       App\Model\WeightManagement {#3846
         id: 9,
         datetime: "2020-05-31 02:44:00",
         weight: "87.00",
         fat_rate: "17.00",
         bmi: "17.00",
         created_at: "2020-05-31 02:44:36",
         updated_at: "2020-05-31 02:44:36",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#3834
           user_id: 1,
           weight_management_id: 9,
         },
       },
       App\Model\WeightManagement {#3847
         id: 10,
         datetime: "2020-06-02 02:03:00",
         weight: "87.80",
         fat_rate: "15.90",
         bmi: "27.00",
         created_at: "2020-06-02 02:03:29",
         updated_at: "2020-06-02 02:03:29",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#3848
           user_id: 1,
           weight_management_id: 10,
         },
       },
     ],
   }

【LARAVEL】【ダイエット支援】データ一覧画面からデータ削除

前回までの状況はこちら。

最新ソースはこちら(gitHub)

https://github.com/takishita2nd/diet-mng

データ削除処理を作成します。

とはいっても、もう追加、編集処理が出来上がっているので、そんなに難しくありませんでした。

ぱぱっとやってしまいます。

    public function delete($id, $user)
    {
        $model = $this->getItemById($id);
        $this->detachToUser($model, $user);
        $model->delete();
    }
    /**
     * データを1件削除する
     */
    public function delete(Request $request)
    {
        $this->weightManagement->delete($request->contents["id"], Auth::user());
        
        return response()->json();
    }
Route::post('api/weight/delete', 'Weight\ApiController@delete');
<template>
    <div>
        <div id="overlay" v-show="show">
            <div id="content">
                <p v-if="error_flg == true" class="error">
                    <ui>
                        <li v-for="error in errors">{{ error }}</li>
                    </ui>
                </p>
                <table class="edit">
                    <tbody>
                        <tr>
                            <td>日時</td>
                            <td>{{contents.date}}</td>
                        </tr>
                        <tr>
                            <td>体重</td>
                            <td>{{contents.weight}}</td>
                        </tr>
                        <tr>
                            <td>体脂肪</td>
                            <td>{{contents.fat_rate}}</td>
                        </tr>
                        <tr>
                            <td>BMI</td>
                            <td>{{contents.bmi}}</td>
                        </tr>
                    </tbody>
                </table>
                <p id="command">
                    <button @click="clickDelete">OK</button>
                    <button @click="closeModal">キャンセル</button>
                </p>
            </div>
        </div>
    </div>
</template>
<script>
export default {
    props: ['show'],
    data() {
        return {
            errors: [],
            error_flg: [],
            param: {},
            contents: {
                date: "",
                weight: "",
                fat_rate: "",
                bmi: "",
            },
        };
    },
    created: function() {
    },
    methods: {
        dataSet: function(data) {
            this.contents = data;
        },
        clickDelete: function() {
            var self = this;
            this.param.contents = this.contents;
            axios.post('api/weight/delete', this.param).then(function(response){
                self.closeModal();
                self.$emit('update');
            }).catch(function(error){
                self.error_flg = true;
                self.errors = error.response.data.errors;
            });
        },
        closeModal: function() {
            this.$parent.showDeleteDialogContent = false;
        },
    }
}
</script>
            <weight-delete-dialog-component ref="deleteDialog" :show="showDeleteDialogContent" @update="invokeUpdateList"></weight-delete-dialog-component>
        onClickDelete: function(id) {
            var editData = {};
            this.datalists.forEach(element => {
                if(element.id == id){
                    editData.id = id;
                    editData.date = element.date;
                    editData.weight = element.weight;
                    editData.fat_rate = element.fat_rate;
                    editData.bmi = element.bmi;
                    return true;
                }
            });
            this.$refs.deleteDialog.dataSet(editData);
            this.showDeleteDialogContent = true;
        },

いいかんじです。

【LARAVEL】【ダイエット支援】データ一覧画面からデータ更新

前回までの状況はこちら。

最新ソースはこちら(gitHub)

https://github.com/takishita2nd/diet-mng

データ編集・更新できるようにします。

まず、編集ダイアログのコンポーネントですが、入力のときと別物にしましょう。

同じコンポーネントも使えるかもしれないけど、いろいろめんどくさい。

<template>
    <div>
        <div id="overlay" v-show="show">
            <div id="content">
                <p v-if="error_flg == true" class="error">
                    <ui>
                        <li v-for="error in errors">{{ error }}</li>
                    </ui>
                </p>
                <table class="edit">
                    <tbody>
                        <tr>
                            <td>体重</td>
                            <td><input type="number" v-model="contents.weight" /></td>
                        </tr>
                        <tr>
                            <td>体脂肪</td>
                            <td><input type="number" v-model="contents.fat_rate" /></td>
                        </tr>
                        <tr>
                            <td>BMI</td>
                            <td><input type="number" v-model="contents.bmi" /></td>
                        </tr>
                    </tbody>
                </table>
                <p id="command">
                    <button @click="clickAdd">入力</button>
                    <button @click="closeModal">閉じる</button>
                </p>
            </div>
        </div>
    </div>
</template>
<script>
export default {
    props: ['show'],
    data() {
        return {
            errors: [],
            error_flg: [],
            param: {},
            contents: {
                id: "",
                weight: "",
                fat_rate: "",
                bmi: "",
            },
        };
    },
    created: function() {
    },
    methods: {
        dataSet: function(data) {
            this.contents = data;
        },
        clickAdd: function() {
            var self = this;
            this.param.contents = this.contents;
            axios.post('api/weight/edit', this.param).then(function(response){
                self.clear();
                self.closeModal();
                self.$emit('update');
            }).catch(function(error){
                self.error_flg = true;
                self.errors = error.response.data.errors;
            });
        },
        closeModal: function() {
            this.$parent.showEditDialogContent = false;
        },
        clear: function() {
            this.contents.weight = "";
            this.contents.fat_rate = "";
            this.contents.bmi = "";
            this.error_flg = false;
            this.errors = [];
        }
    }
}
</script>

そして、それを使用する側の親コンポーネント。

        <div>
            <weight-input-dialog-component :show="showInputDialogContent" @update="invokeUpdateList"></weight-input-dialog-component>
            <weight-edit-dialog-component ref="editDialog" :show="showEditDialogContent" @update="invokeUpdateList"></weight-edit-dialog-component>
        </div>
        onClickEdit: function(id) {
            var editData = {};
            this.datalists.forEach(element => {
                if(element.id == id){
                    editData.id = id;
                    editData.weight = element.weight;
                    editData.fat_rate = element.fat_rate;
                    editData.bmi = element.bmi;
                    return true;
                }
            });
            this.$refs.editDialog.dataSet(editData);
            this.showEditDialogContent = true;

ポイントは、編集ダイアログにデータを渡すとき。

親コンポーネントから子コンポーネントのメソッドをコールしています。

これは、子コンポーネントを定義するときに、

ref="editDialog"

と書くことで、

            this.$refs.editDialog.dataSet(editData);

と、子コンポーネントのメソッドを実行できます。

その引数でパラメータを渡して、表示させています。

あとは、今までどおり。

Repository

    public function edit($id, $weight, $fat_rate, $bmi)
    {
        $model = $this->getItemById($id);
        $model->weight = $weight;
        $model->fat_rate = $fat_rate;
        $model->bmi = $bmi;
        $model->save();
    }

    public function getItemById($id)
    {
        return WeightManagement::where(['id' => $id])->first();
    }

controller

    /**
     * データを1件更新する
     */
    public function edit(Request $request)
    {
        $param = $this->weightManagement->getParam();
        $this->weightManagement->edit( $request->contents["id"], 
            $request->contents["weight"],
            $request->contents["fat_rate"],
            $request->contents["bmi"]
        );
        
        return response()->json();
    }
Route::post('api/weight/edit', 'Weight\ApiController@edit');

【LARAVEL】【ダイエット支援】データ一覧画面からデータ入力

前回までの状況はこちら。

最新ソースはこちら(gitHub)

https://github.com/takishita2nd/diet-mng

データ一覧画面からデータ入力を行います。

<template>
    <div>
        <div>
            <p id="navi">> <a href="/home">HOME</a></p>
            <p id="inputbutton">
                <button @click="onClickInput">データ入力</button>
            </p>
            <table class="weightlist">
                <tbody>
                    <tr>
                        <th class="date">日時</th>
                        <th class="weight">体重(kg)</th>
                        <th class="fat_rate">体脂肪(%)</th>
                        <th class="bmi">BMI</th>
                        <th class="edit"></th>
                        <th class="delele"></th>
                    </tr>
                    <tr v-for="data in datalists">
                        <td class="date">{{ data.date}}</td>
                        <td class="weight">{{ data.weight}}</td>
                        <td class="fat_rate">{{ data.fat_rate}}</td>
                        <td class="bmi">{{ data.bmi}}</td>
                        <td class="edit"><a href="">Edit</a></td>
                        <td class="delele"><a href="">Delete</a></td>
                    </tr>
                </tbody>
            </table>
        </div>
        <div>
            <weight-input-dialog-component :show="showDialogContent" @update="invokeUpdateList"></weight-input-dialog-component>
        </div>
    </div>
</template>

入力ダイアログ画面は、すでに作成してあるコンポーネントを流用します。

しかし、この一覧画面から入力した場合は、入力後、リストを更新する必要があります。

なので、入力ダイアログコンポーネントから親コンポーネントで信号を送って、リスト更新処理を走らせるようにします。

入力処理はこんな感じ。

        clickAdd: function() {
            var self = this;
            this.param.contents = this.contents;
            axios.post('api/weight/add', this.param).then(function(response){
                self.clear();
                self.closeModal();
                self.$emit('update');
            }).catch(function(error){
                self.error_flg = true;
                self.errors = error.response.data.errors;
            });
        },

ポイントは、親側(データ一覧画面)側で

<weight-input-dialog-component :show="showDialogContent" @update="invokeUpdateList"></weight-input-dialog-component>

@update=”invokeUpdateList”の部分。

これがある状態で、子コンポーネントから

this.$emit('update');

を実行すると、親コンポーネントのinvokeUpdateList()メソッドが実行されます。

    created: function() {
        this.updateList();
    },
    methods: {
        onClickInput: function() {
            this.showDialogContent = true;
        },
        invokeUpdateList: function() {
            this.updateList();
        },
        updateList: function() {
            this.datalists = [];
            var self = this;
            axios.post('api/weight/list').then(function(response){
                response.data.dataLists.forEach(element => {
                    self.datalists.push({
                        date: element.datetime,
                        weight: element.weight,
                        fat_rate: element.fat_rate,
                        bmi: element.bmi
                    })
                });
            }).catch(function(error){
            });
        }
    }

こんな感じで更新処理を行えばOK。

ちなみに、ダッシュボード画面(:@updateの記載がない)に対して$emit(‘update’)を行っても特にエラーにはなりませんでした。

子コンポーネント定義に書かなければ、emitしても何事もなく動くみたいです。

【ダイエット支援】画面構成を考える。

とりあえず、環境を初期設定しました。

Auth機能を設定。

詳細はあとで作成する。

ログインするとこんな感じ。

とりあえず、体重などを記録して表示させたいと思っているので、画面はこんな感じかな。

ダッシュボードにはこんな感じで画面を作る。

クイック入力をクリックすると、ダイアログを表示する。

パパっと入力できるように。

詳細をクリックするとこんな感じの画面に遷移する。

こんな感じでリスト表示。

ここでデータ入力、編集、削除ができる。

操作はダイアログかなぁ。

ページャーもいるよね。

あとはグラフ表示を行うのにどのライブラリを使うかだな。

今後の予定としては、

  • 画面のMOCKを作成
  • WebAPIを作成
  • グラフのライブラリを探す

こんな感じかなぁ。

ぼちぼちとりかかりますか。