特に特筆することはやっていないので、進捗報告だけ。
https://github.com/takishita2nd/diet-mng
とりあえず画面の表示だけ。
全チェック処理とか、テンプレートに移す処理とか、データを削除する処理とかは次回やります。
あと、管理者アカウントを別に作って、他の人がこのページにアクセスできないようにする必要もありますね。
特に特筆することはやっていないので、進捗報告だけ。
https://github.com/takishita2nd/diet-mng
とりあえず画面の表示だけ。
全チェック処理とか、テンプレートに移す処理とか、データを削除する処理とかは次回やります。
あと、管理者アカウントを別に作って、他の人がこのページにアクセスできないようにする必要もありますね。
ここもサクッと作成できると思う。
namespace App\Repository;
use App\Model\EatingHistoryItem;
class EatingManagementRepository
{
private $templateParamNames = ['item', 'protein', 'liqid', 'carbo', 'calorie'];
/**
* ヒストリにデータを1件追加する
*/
public function addHistory($param, $user)
{
$model = new EatingHistoryItem();
foreach($this->templateParamNames as $name)
{
$model->$name = $param[$name];
}
$model->save();
$this->attachToUser($model, $user);
}
class ApiController extends Controller
{
/**
* データを1件登録する
*/
public function add(Request $request)
{
$paramNames = $this->eatingManagement->getParam();
$param = [];
foreach($paramNames as $name) {
$param[$name] = $request->contents[$name];
}
$this->eatingManagement->add($param, Auth::user(), $request->contents['timezone']);
$this->eatingManagement->addHistory($param, Auth::user());
return response()->json();
}
入力したデータをそのまま履歴にも記入する、という処理ですな。
ここまではサクッとできたけど、次回からはかなりヘビーになると思う。
データベースをサクッと作成していきます。
$ php artisan make:migration create_eating_history_item
class CreateEatingHistoryItem extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('eating_history_items', function (Blueprint $table) {
$table->bigIncrements('id');
$table->double('protein');
$table->double('liqid');
$table->double('carbo');
$table->double('calorie');
$table->timestamps();
$table->engine = 'InnoDB';
$table->charset = 'utf8mb4';
$table->collation = 'utf8mb4_unicode_ci';
});
Schema::create('eating_history_item_user', function (Blueprint $table) {
$table->integer('user_id')
->foreign('user_id')
->references('id')->on('users')
->onDelete('cascade');
$table->integer('eating_history_item_id')
->foreign('eating_history_item_id')
->references('id')->on('eating_history_items')
->onDelete('cascade');
$table->engine = 'InnoDB';
$table->charset = 'utf8mb4';
$table->collation = 'utf8mb4_unicode_ci';
});
Schema::create('eating_template_items', function (Blueprint $table) {
$table->bigIncrements('id');
$table->double('protein');
$table->double('liqid');
$table->double('carbo');
$table->double('calorie');
$table->timestamps();
$table->engine = 'InnoDB';
$table->charset = 'utf8mb4';
$table->collation = 'utf8mb4_unicode_ci';
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('eating_template_items');
Schema::dropIfExists('eating_history_item_user');
Schema::dropIfExists('eating_history_items');
}
}
$ php artisan migrate
class EatingHistoryItem extends Model
{
protected $table = 'eating_history_items';
}
class EatingTemplateItem extends Model
{
protected $table = 'eating_template_items';
}
class User extends Authenticatable
{
public function EatingHistoryItems()
{
return $this->belongsToMany('App\Model\EatingHistoryItem');
}
}
データの検索は、テンプレートからだけでなく、ユーザーが入力したヒストリデータからも取得できることを想定して構築してみました。
あと、開発環境がUbuntu 18.04から20.04になったのですが、
PHPのバージョンが7.2から7.4に変わりました。
たぶん、このままだとLaravelからデータベース(mysql)にアクセスできない(could not find driver)ので、php-mysqlをインストールし直す必要があるようです。
$ sudo apt-get install php-mysql
たぶん、前回の記事の内容だけではコーディングはまだできなくって、
もっと具体的に動作の仕組みを考えなくちゃいけないと思いまして。
テキスト窓に入力したかどうかはVue.jsではv-on:changeで処理を起動することができるみたいなので、これを使用します。
基本的には入力済みの食品・栄養素の情報(テンプレート)からデータを抽出して候補がある物から選択するという形にしたいと思います。
ただ、そう言った食品や栄養素の情報を全部用意するのは大変なので、ユーザーが入力した情報を記憶しておいて(ヒストリ)、管理者が正式データとして登録する、という仕組みにしたいと思います。
なので、管理者画面が必要になりますね。
データベースはテンプレートとヒストリの二つ用意しておきたいと思います。
ここらへん、本当はもっと詰めなくちゃいけない所ですが、今回はあとでどうにでもできるように、リスクが少ない方法を選択したいと思います。
あとでデータベース構成を変えるとなるとコードの修正も大変なので。
なので、今後の作業はこんな感じです。
よし、これでいきましょう。
一通り食事管理機能は完成したのですが、
正直、いまいち使いづらいです。
数値を毎回入力しなければならないので。
過去に入力したデータを再利用できないかな、と思っているのですが、
調べてみると、HTMLのinputタグにはtype=searchというものがありまして、
<input type="search" v-model="contents.item" autocomplete="on" list="keyword"/>
<datalist id="keyword">
<option value="札幌" />
<option value="札駅" />
<option value="新さっぽろ" />
<option value="東札幌" />
<option value="札束" />
</datalist>
こんな感じでautocomplete=”on” list=”キーワード”と記入すると、
こんな感じでdatalistタグのid=”キーワード”の内容が入力候補として表示されます。
これをうまく使えないかと。
品名の一部を入力→入力履歴を検索→候補を表示→履歴からデータを入力
という感じで、うまく処理できないかと思っています。
本来なら栄養素情報を別テーブルにしてそれを参照するという、正規化が必要なのだと思いますが、
いまからデータベースに変更を入れるのは、既存機能の大規模改修が発生(めんどくさい)ので、
それはそのままに、履歴検索用のテーブルを用意することにします。
それがあれば、入力履歴検索用のAPIを作成すれば行けるような気がします。
datalistの中のoptionタグがv-forでリストを反映させることができると思います。
ただ、大量のoptionが画面に表示されても鬱陶しいので、例えば、検索結果が10件以上だったらあえて表示させない、というのも、一つの手かもしれません。
とりあえず、こんな方針でやってみますか。
次回から着手します。
前回までの状況はこちら
最新ソースはこちら(gitHub)。
https://github.com/takishita2nd/diet-mng
前回計算した内容をデータベースに保存・読み出しを行う機能を実装します。
まずはデータベースのマイグレーションを作成。
class CreateEatingTarget extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('eating_targets', function (Blueprint $table) {
$table->bigIncrements('id');
$table->double('protein');
$table->double('liqid');
$table->double('carbo');
$table->double('calorie');
$table->timestamps();
$table->engine = 'InnoDB';
$table->charset = 'utf8mb4';
$table->collation = 'utf8mb4_unicode_ci';
});
Schema::create('eating_target_user', function (Blueprint $table) {
$table->integer('user_id')
->foreign('user_id')
->references('id')->on('users')
->onDelete('cascade');
$table->integer('eating_target_id')
->foreign('eating_target_id')
->references('id')->on('eating_targets')
->onDelete('cascade');
$table->engine = 'InnoDB';
$table->charset = 'utf8mb4';
$table->collation = 'utf8mb4_unicode_ci';
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('eating_target_user');
Schema::dropIfExists('eating_targets');
}
}
$ php artisan migrate
このテーブルのモデルを作成。
class EatingTarget extends Model
{
protected $table = 'eating_targets';
public function users()
{
return $this->belongsToMany('App\User');
}
}
Userテーブルからのリレーションも作成します。
class User extends Authenticatable
{
public function EatingTargets()
{
return $this->belongsToMany('App\Model\EatingTarget');
}
このデータベースにアクセスするためのAPIを作成します。
セット処理。
Route::post('api/eating/settarget', 'Eating\ApiController@setTarget');
class ApiController extends Controller
{
/**
* 目標栄養素を設定する
*/
public function setTarget(Request $request)
{
$paramNames = $this->eatingManagement->getTargetParam();
$param = [];
foreach($paramNames as $name) {
$param[$name] = $request->contents[$name];
}
$this->eatingManagement->setTarget($param, Auth::user());
return response()->json();
}
use App\Model\EatingTarget;
class EatingManagementRepository
{
private $targetParamNames = ['protein', 'liqid', 'carbo', 'calorie'];
public function setTarget($param, $user)
{
$model = $user->EatingTargets()->first();
if(is_null($model)) {
$model = new EatingTarget();
}
foreach($this->targetParamNames as $name)
{
$model->$name = $param[$name];
}
$model->save();
$this->attachToUser($model, $user);
}
public function getTargetParam()
{
return $this->targetParamNames;
}
これをVue.jsの処理に組み込みます。
methods: {
clickAdd: function() {
var self = this;
this.param.contents = this.contents;
axios.post('/api/eating/settarget', this.param).then(function(response){
self.closeModal();
self.$emit('update');
}).catch(function(error){
self.error_flg = true;
self.errors = error.response.data.errors;
});
},
次は、このデータを読み出す処理を実装します。
グラフデータを読み出すAPIがすでにありますので、これにデータを追加します。
class ApiController extends Controller
{
/**
* グラフ用データを取得する
*/
public function graph(Request $request)
{
return response()->json([
'data' => $this->eatingManagement->getDaily(Auth::user(), $request->contents['date']),
'target' => $this->eatingManagement->getTarget(Auth::user())
]);
}
class EatingManagementRepository
{
public function getTarget($user)
{
return $user->EatingTargets()->first();
}
graphUpdate: function() {
var ctx = document.getElementById("eating");
var self = this;
this.contents.date = this.todayDate;
this.param.contents = this.contents;
this.datasets = [];
axios.post('api/eating/graph', this.param).then(function(response){
if(response.data.data != null) {
self.datasets.push(Math.ceil(response.data.data.protein / response.data.target.protein * 100));
self.datasets.push(Math.ceil(response.data.data.liqid / response.data.target.liqid * 100));
self.datasets.push(Math.ceil(response.data.data.carbo / response.data.target.carbo * 100));
self.datasets.push(Math.ceil(response.data.data.calorie / response.data.target.calorie * 100));
var myChart = new Chart(ctx, {
なかなかいい感じなので、これで行きましょう。
前回までの状況はこちら。
最新ソースはこちら(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="inputParameter.height" /></td>
</tr>
<tr>
<td>体重</td>
<td><input type="number" v-model="inputParameter.weight" /></td>
</tr>
<tr>
<td>年齢</td>
<td><input type="number" v-model="inputParameter.age" /></td>
</tr>
<tr>
<td>アクティブ度</td>
<td>
<select name="active" v-model="inputParameter.active">
<option value="1" selected>低</option>
<option value="2">中</option>
<option value="3">高</option>
</select>
</td>
</tr>
<tr>
<td>目的</td>
<td>
<select name="target" v-model="inputParameter.target">
<option value="1" selected>維持</option>
<option value="2">減量</option>
<option value="3">増量</option>
</select>
</td>
</tr>
</tbody>
</table>
<p />
<table class="edit">
<tbody>
<tr>
<td>目標カロリー</td>
<td>{{calorie}} cal</td>
</tr>
<tr>
<td>タンパク質</td>
<td>{{protein}} g</td>
</tr>
<tr>
<td>脂質</td>
<td>{{liquid}} g</td>
</tr>
<tr>
<td>炭水化物</td>
<td>{{carbo}} g</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: {},
inputParameter: {
height: 0,
weight: 0,
age: 0,
active: 1,
target: 1,
},
contents: {
calorie: 0,
protein: 0,
liquid: 0,
carbo: 0,
},
};
},
created: function() {
this.clear();
},
methods: {
clickAdd: function() {
var self = this;
this.param.contents = this.contents;
axios.post('/api/eating/settarget', this.param).then(function(response){
}).catch(function(error){
self.error_flg = true;
self.errors = error.response.data.errors;
});
},
closeModal: function() {
this.$parent.showCalcCalorieContent = false;
},
clear: function() {
this.inputParameter.height = 0;
this.inputParameter.weight = 0;
this.inputParameter.age = 0;
this.inputParameter.active = "1";
this.inputParameter.target = "1";
this.contents.calorie = 0;
this.contents.protein = 0;
this.contents.liquid = 0;
this.contents.carbo = 0;
this.error_flg = false;
this.errors = [];
}
}
}
</script>
ボタンはここに配置ました。
身長、体重、年齢と、アクティブ度と目標を入力してもらって、目標となるカロリーと摂取栄養素の量を計算します。
計算式はそんなに難しくないので、フロントエンド側で計算して表示させちゃいます。
computed: {
calorie: function() {
var cal = 10 * this.inputParameter.weight + 6.25 * this.inputParameter.height - 5 * this.inputParameter.age + 5;
var k = 1;
// アクティブ度の計算
switch(this.inputParameter.active){
case "1": k = 1.2; break;
case "2": k = 1.55; break;
case "3": k = 1.725; break;
}
cal = cal * k;
// 目標の計算
switch(this.inputParameter.target){
case "1": k = 1; break;
case "2": k = 0.8; break;
case "3": k = 1.2; break;
}
this.contents.calorie = Math.ceil(cal * k);
return this.contents.calorie;
},
protein: function() {
this.contents.protein = this.inputParameter.weight * 2;
return this.contents.protein;
},
liquid: function() {
this.contents.liquid = Math.ceil(this.contents.calorie * 0.25 / 9);
return this.contents.liquid;
},
carbo: function() {
this.contents.carbo = Math.ceil((this.contents.calorie - this.contents.protein * 4 - this.contents.liquid * 9) / 4);
return this.contents.carbo;
},
},
まず、computedに記載することで、入力パラメータからすぐさま計算処理を行い、結果を反映してくれます。
カロリーの計算式は、
10×体重(g)+6.25×身長(cm)-5×年齢(歳)+5
これにアクティブ度と目的に合わせて係数を掛けます。
アクティブ度
目的
これらは以下で紹介した本の中にあります。
フロントエンド側はこれで完成。
次はこの計算結果を保持するデータベース周りのバックエンド側を作成していきます。
前回までの状況はこちら
前回のままだと、摂取量をそのまま数字としてグラフに表示させているだけなので、
この数字が適正量なのかを判断するために、適正値との割合をパーセンテージにして表示させたいと思います。
といっても、どれくらいが適正量なのか、というのが、体型などに左右されやすいものなので、
こちらの記事の計算式を参考にして、
体重60kg、摂取カロリー2000kcalとして計算すると、
この数字を直に与えました。
baseData: {
protein: 120.0,
liqid: 55.5,
carbo: 255.0,
calorie: 2000.0
},
これ、ちゃんとした条件をもとに計算する仕組みが必要だね。
self.datasets.push(response.data.data.protein / self.baseData.protein * 100);
self.datasets.push(response.data.data.liqid / self.baseData.liqid * 100);
self.datasets.push(response.data.data.carbo / self.baseData.carbo * 100);
self.datasets.push(response.data.data.calorie / self.baseData.calorie * 100);
これでグラフがパーセンテージになります。
なので、どの栄養素が足りてなくて、どの栄養素を取りすぎているかがわかります。
前回までの状況はこちら
最新ソースはこちら(gitHub)
https://github.com/takishita2nd/diet-mng
データ削除処理を作成していきます。
まずはDeleteをクリックしたときの処理。
やり方はEditと同じです。
EatingDetailComponent.vue
onClickDelete: function(timezone, id) {
var deleteData = {};
this.datalists[timezone].forEach(element => {
if(element.id == id){
deleteData.id = id;
deleteData.date = this.date;
deleteData.item = element.item;
deleteData.timezone = timezone + 1;
deleteData.protein = element.protein;
deleteData.liqid = element.liqid;
deleteData.carbo = element.carbo;
deleteData.calorie = element.calorie;
return true;
}
});
this.$refs.deleteDialog.dataSet(deleteData);
this.showDeleteDialogContent = true;
},
ダイアログ処理は体重管理のものを流用しています。
EatingDeleteDialogComponent.vue
<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.item}}</td>
</tr>
<tr>
<td>時間帯</td>
<td>{{contents.time}}</td>
</tr>
<tr>
<td>タンパク質</td>
<td>{{contents.protein}}</td>
</tr>
<tr>
<td>脂質</td>
<td>{{contents.liqid}}</td>
</tr>
<tr>
<td>炭水化物</td>
<td>{{contents.carbo}}</td>
</tr>
<tr>
<td>カロリー</td>
<td>{{contents.calorie}}</td>
</tr>
</tbody>
</table>
<p id="command">
<button @click="clickDelete">削除</button>
<button @click="closeModal">閉じる</button>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['show'],
data() {
return {
errors: [],
error_flg: [],
param: {},
contents: {},
};
},
created: function() {
},
methods: {
dataSet: function(data) {
this.contents = data;
switch(this.contents.timezone) {
case 1:
this.contents.time = "朝";
break;
case 2:
this.contents.time = "昼";
break;
case 3:
this.contents.time = "夜";
break;
case 4:
this.contents.time = "間食";
break;
}
},
clickDelete: function() {
var self = this;
this.param.contents = this.contents;
axios.post('/api/eating/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>
次はAPIの処理です。
これも体重管理の処理とほぼ同じ。
ApiController.php
/**
* データを一件削除する
*/
public function delete(Request $request)
{
$this->eatingManagement->delete(Auth::user(), $request->contents['id']);
return response()->json();
}
EatingManagementRepository.php
/**
* データを一件削除する
*/
public function delete($user, $id)
{
$model = $user->EatingManagements()->where('id', $id)->first();
$timezone = $model->timezones()->first();
$this->detachToUser($model, $user);
$this->detachToTimezone($model, $timezone);
$model->delete();
}
違いはtimezoneテーブルとのリンクを削除する処理が増えたぐらいで、やっていることはそんなに難しくないです。
すんなり完成しました。
これでCURD処理一通り完成したので、次回はグラフ処理を作成していこうと思います。
前回までの状況はこちら。
最新ソースはこちら(gitHub)
https://github.com/takishita2nd/diet-mng
データ編集処理を作成していきます。
やり方は、体重管理機能のものと同じです。
EatingEditDialogComponent.vue
<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="date" v-model="contents.date" v-if="datehold" readonly>
<input type="date" v-model="contents.date" v-else>
</td>
</tr>
<tr>
<td>品名</td>
<td><input type="text" v-model="contents.item" /></td>
</tr>
<tr>
<td>時間帯</td>
<td>
<select name="timezone" v-model="contents.timezone">
<option value="1" selected>朝</option>
<option value="2">昼</option>
<option value="3">夜</option>
<option value="4">間食</option>
</select>
</td>
</tr>
<tr>
<td>タンパク質</td>
<td><input type="number" v-model="contents.protein" /></td>
</tr>
<tr>
<td>脂質</td>
<td><input type="number" v-model="contents.liqid" /></td>
</tr>
<tr>
<td>炭水化物</td>
<td><input type="number" v-model="contents.carbo" /></td>
</tr>
<tr>
<td>カロリー</td>
<td><input type="number" v-model="contents.calorie" /></td>
</tr>
</tbody>
</table>
<p id="command">
<button @click="clickEdit">編集</button>
<button @click="closeModal">閉じる</button>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['show', 'date', 'datehold'],
data() {
return {
errors: [],
error_flg: [],
param: {},
contents: {},
};
},
methods: {
dataSet: function(data) {
this.contents = data;
},
clickEdit: function() {
var self = this;
this.param.contents = this.contents;
axios.post('/api/eating/update', 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.date = this.date;
this.contents.item = "";
this.contents.timezone = 1;
this.contents.protein = "";
this.contents.liqid = "";
this.contents.carbo = "";
this.contents.calorie = "";
this.error_flg = false;
this.errors = [];
}
}
}
</script>
EatingDetailComponent.vue
<eating-edit-dialog-component ref="editDialog" :show="showEditDialogContent" :date="date" :datehold=true @update="invokeUpdateList"></eating-edit-dialog-component>
onClickEdit: function(timezone, id) {
var editData = {};
this.datalists[timezone].forEach(element => {
if(element.id == id){
editData.id = id;
editData.date = this.date;
editData.item = element.item;
editData.timezone = timezone + 1;
editData.protein = element.protein;
editData.liqid = element.liqid;
editData.carbo = element.carbo;
editData.calorie = element.calorie;
return true;
}
});
this.$refs.editDialog.dataSet(editData);
this.showEditDialogContent = true;
},
Editボタンをクリックすると、detail側でEditダイアログに表示するパラメータを作成し、Editダイアログに渡します。
refパラメータを使用することで、親から子のモジュールの関数を呼び出すことができるので、これを利用してパラメータ一式を子モジュールに渡します。
編集ダイアログで編集をクリックすると、APIを呼び出してデータの更新を行います。
ApiController.php
/**
* データを一件取得する
*/
public function update(Request $request)
{
$paramNames = $this->eatingManagement->getParam();
$param = [];
foreach($paramNames as $name) {
$param[$name] = $request->contents[$name];
}
$this->eatingManagement->update($param, Auth::user(), $request->contents['id'], $request->contents['timezone']);
return response()->json();
}
EatingManagementRepository.php
/**
* データを一件取得する
*/
public function update($param, $user, $id, $timezone)
{
$model = $user->EatingManagements()->where('id', $id)->first();
foreach($this->paramNames as $name)
{
$model->$name = $param[$name];
}
$model->save();
$oldtime = $model->timezones()->first();
$newtime = Timezone::where('id', $timezone)->first();
$this->detachToTimezone($model, $oldtime);
$this->attachToTimezone($model, $newtime);
}
データを更新するときは、IDからデータベースのデータを取得し、これのデータを上書きすることで更新できます。
ただ、時間帯の更新というのもありえるので、timezonesテーブルとのリンクを再構築しなければなりません。
いったん、リレーションからtimezonesとデタッチし、新しいtimezonesとアタッチします。
これが終わったら、入力と同様にダイアログを非表示にし、detail画面を更新します。
ちょっと表示に手間取ったけど、できました。