前回はデータ一覧を、カテゴリーや選択項目などで絞り込んで検索する機能を開発しました。
今回はまずヘッダーメニューをハンバーガーメニューへ修正したあと、収入や支出の月ごとの合計金額の推移グラフを表示する年間レポートページを作成します。
ハンバーガーメニューへ修正
現在、ヘッダーメニューには「ホームへ戻る」「アカウント情報」「ログアウト」のアイコンがありますが、この他に「選択項目編集」「選択項目別レポート」「年間収支レポート」を追加したいためPCもスマホもハンバーガーメニューへ修正します。
<!--書き換え前-->
<header class="l-header">
<h1 class="l-header__title"><a href="./index.php">家計簿アプリ</a></h1>
<div class="l-header__icon">
<a href="./index.php">
<i class="fa-solid fa-house"></i>
</a>
<a href="./account.php">
<i class="fa-solid fa-user"></i>
</a>
<a href="./logout.php" id="logoutButton" onclick="logoutConfirm();">
<i class="fa-solid fa-arrow-right-from-bracket"></i>
</a>
</div>
</header>
上記部分を以下に書き換えます。
<header class="l-header">
<h1 class="l-header__title"><a href="./index.php">家計簿アプリ</a></h1>
<div class="l-header__menu">
<div class="p-hamburger-button" onclick="onToggleNavigation();" id="hamburgerButton">
<i class="fa-solid fa-list-ul"></i>
<i class="fa-solid fa-xmark"></i>
</div>
<div class="c-layer"></div>
<ul class="p-navigation" id="navigation">
<li>
<a href="./index.php">
<i class="fa-solid fa-house"></i>ホーム画面
</a>
</li>
<li>
<a href="./account.php">
<i class="fa-solid fa-user"></i>ユーザー情報</a>
</li>
<li>
<a href="./item-edit.php?editItem=1">
<i class="fa-solid fa-pen"></i>選択項目の編集</a>
</li>
<li>
<a href="./item-report.php"><i class="fa-solid fa-chart-simple"></i>項目別レポート</a>
</li>
<li>
<a href="./amount-report.php"><i class="fa-solid fa-chart-simple"></i>年間収支レポート</a>
</li>
<li>
<li>
<a href="./logout.php" id="logoutButton" onclick="logoutConfirm();">
<i class="fa-solid fa-arrow-right-from-bracket"></i>ログアウト
</a>
</li>
</ul>
</div>
</header>
CSSはすでに指定しています。
続いてハンバーガーボタンのクリックイベントを追加します。
const onToggleNavigation = () => {
const hamburgerButton = document.getElementById("hamburgerButton");
const navigation = document.getElementById("navigation");
if (hamburgerButton.classList.contains("is-open")) {
//閉じるボタンクリック時
hamburgerButton.classList.remove("is-open");
navigation.classList.remove("is-open");
body.classList.remove("is-fixed");
hamburgerButton.classList.add("is-close");
navigation.classList.add("is-close");
} else {
//開くボタンクリック時
hamburgerButton.classList.remove("is-close");
navigation.classList.remove("is-close");
hamburgerButton.classList.add("is-open");
navigation.classList.add("is-open");
body.classList.add("is-fixed");
}
};
ハンバーガーメニューへの移行は完了です。
最後に「項目別レポート」をクリックするとパラメータがないのでホームに返されてしまいます。
パラメータがないときの処理を選択項目グラフページに追加します。
//以下をelseif内から移動させる
$table_list = ['spending_category', 'income_category', 'creditcard', 'qr'];
if (isset($_POST['year']) && isset($_POST['item']) && isset($_POST['item_table'])) :
//検索ボタンが押下されたとき(省略)
elseif (isset($_GET['year']) && isset($_GET['item']) && $_GET['item'] !== '' && isset($_GET['num']) && $_GET['num'] < 4) :
//円グラフ横の表から遷移されたとき(省略)
else :
//追記↓↓
$stmt = $db->prepare('SELECT id FROM spending_category WHERE user_id = ? ORDER BY id ASC LIMIT 1');
$stmt->bind_param('i', $user_id);
$count = sql_check($stmt, $db);
$stmt->bind_result($id);
$stmt->fetch();
$stmt->close();
$year = date('Y');
$item = $id;
$num = 0;
$item_table = $table_list[$num];
//どちらにも当てはまらない場合はエラー付きパラメータを付けて返す(以下2行削除)
// header('Location: ./index.php?dataOperation=error');
// exit();
endif;
以上でパラメータがないときの遷移では、支出カテゴリーの1番早く登録された項目の当年分を表示するようにしました。
年間レポートページ実装
それではハンバーガーメニュー内の「年間収支レポート」から遷移し、収支の月別合計金額推移ページを実装します。
プログラムは選択項目別の金額推移ページを実装した際と似ているので、「item-report.php」をコピーして「amount-report.php」という名前に変更します。
そして以下の3箇所を削除します。
・ファイルの冒頭共通ファイル読み込み以外のPHPプログラム(if文部分)
・<div class=”bar-graph” id=”barGraph”>〜</div>内のPHPプログラム部分
・<section class=”p-section p-section__item-report-datalist”>〜</section>
<?php
include_once('./session.php');
require_once('./dbconnect.php');
include_once('./functions.php');
$page_title = "年間レポート";
include_once('./header.php');
?>
<main class="l-main">
<section class="p-section p-section__bar-graph">
<form class="p-form--bar-graph u-flex-box" action="" method="POST" name="graphSearch">
<input type="hidden" id="itemTable" name="item_table" value="<?php echo isset($item_table) ? $item_table : 'spending_category'; ?>">
<select name="year">
<?php for ($i = 2020; $i <= date('Y'); $i++) : ?>
<option value="<?php echo $i; ?>" <?php echo $year == $i ? 'selected' : ''; ?>><?php echo $i; ?>年</option>
<?php endfor; ?>
</select>
<select name="item" onchange="onChangeItem();">
<optgroup id="defaultGroup" label="支出カテゴリー">
<?php
$stmt = $db->prepare('SELECT id, name FROM spending_category WHERE user_id=?');
$stmt->bind_param('i', $user_id);
$stmt->execute();
$stmt->bind_result($id, $name);
while ($stmt->fetch()) : ?>
<option value="<?php echo $id; ?>" <?php echo ($item_table === 'spending_category' && $item == $id) ? 'selected' : ''; ?>><?php echo $name; ?></option>
<?php endwhile; ?>
</optgroup>
<optgroup label="収入カテゴリー">
<?php
$stmt = $db->prepare('SELECT id, name FROM income_category WHERE user_id=?');
$stmt->bind_param('i', $user_id);
$stmt->execute();
$stmt->bind_result($id, $name);
while ($stmt->fetch()) : ?>
<option value="<?php echo $id; ?>" <?php echo ($item_table === 'income_category' && $item == $id) ? 'selected' : ''; ?>><?php echo $name; ?></option>
<?php endwhile; ?>
</optgroup>
<optgroup label="クレジットカード">
<?php
$stmt = $db->prepare('SELECT id, name FROM creditcard WHERE user_id=?');
$stmt->bind_param('i', $user_id);
$stmt->execute();
$stmt->bind_result($id, $name);
while ($stmt->fetch()) : ?>
<option value="<?php echo $id; ?>" <?php echo ($item_table === 'creditcard' && $item == $id) ? 'selected' : ''; ?>><?php echo $name; ?></option>
<?php endwhile; ?>
</optgroup>
<optgroup label="スマホ決済">
<?php
$stmt = $db->prepare('SELECT id, name FROM qr WHERE user_id=?');
$stmt->bind_param('i', $user_id);
$stmt->execute();
$stmt->bind_result($id, $name);
while ($stmt->fetch()) : ?>
<option value="<?php echo $id; ?>" <?php echo ($item_table === 'qr' && $item == $id) ? 'selected' : ''; ?>><?php echo $name; ?></option>
<?php endwhile; ?>
</optgroup>
</select>
<input type="submit" class="c-button c-button--bg-blue" value="検索" onclick="">
</form>
<div class="bar-graph" id="barGraph">
<canvas id="canvas"></canvas>
<span><i class="fa-regular fa-hand-pointer"></i></span>
</div>
</section>
<section class="p-section p-section__back-home">
<a href="./index.php" class="c-button c-button--bg-gray">ホームに戻る</a>
</section>
</main>
<footer id="footer" class="l-footer">
<p>家計簿アプリ|2022</p>
</footer>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="./js/import.js"></script>
<script src="./js/functions.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.1"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
<script>
const itemTable = document.getElementById('itemTable');
const selectForm = document.graphSearch.item;
//key:テーブル名、値:optgroupのlabelの連想配列
const tableList = {
'spending_category': '支出カテゴリー',
'income_category': '収入カテゴリー',
'creditcard': 'クレジットカード',
'qr': 'スマホ決済'
};
const onChangeItem = () => {
//選択されたoptionが何番目の要素か
const num = document.graphSearch.item.selectedIndex;
//選択されたoptionの親要素optgroupのラベル取得
const optGroupLabel = selectForm.options[num].parentNode.label;
//取得したoptgroupラベルと一致する連想配列の値を検索し、そのkey名を返す
const key = Object.keys(tableList).find((key) => tableList[key] === optGroupLabel);
//テーブル名を入れるinputのvalueを書き換え
itemTable.value = key;
}
const canvas = document.getElementById('canvas');
window.onload = function() {
//canvasの高さと幅指定
const containerWidth = document.getElementById('barGraph').clientWidth;
if (window.outerWidth < 600) {
canvas.style.width = '850px';
} else {
canvas.style.width = containerWidth + 'px';
}
canvas.style.height = '400px';
//y軸目盛の間隔と最大値計算処理
const aryMax = (a, b) => { //配列の頭から二つずつ比較して最大値を返す関数
return Math.max(a, b);
}
//金額配列に対して上記関数を実行し最大値を格納
const maxVal = <?php echo $json_amount; ?>.reduce(aryMax);
//y軸目盛間隔、最大値切り上げのための割る数、y軸の最大値変数定義
let stepVal, roundVal;
//最大値によってy軸目盛間隔、最大値切り上げのための割る数を設定
if (maxVal < 1000) { //0-999円
stepVal = 100;
roundVal = 100; //100で割る
} else if (maxVal < 10000) { //1,000-9,999円
stepVal = 1000;
roundVal = 1000; //1000で割る
} else if (maxVal < 50000) { //10,000-49,999円
stepVal = 5000;
roundVal = 5000; //5000で割る
} else if (maxVal < 100000) { //50,000-99,999円
stepVal = 10000;
roundVal = 10000; //10000で割る
} else { //100,000円以上
stepVal = 100000;
roundVal = 100000; //10000で割る
}
//金額の最大値によってy軸の最大値を設定
//一度最大金額を切り上げたい桁数が小数点以下になるように割ってから切り上げ→割った数をかけて桁数を戻す
let ymax = Math.ceil(maxVal / roundVal) * roundVal;
if (maxVal === 0) { //0円のとき
ymax = 1000; //強制的にy軸の最大値は1000に設定
} else if (maxVal == ymax) { //金額最大値とy軸最大値が同じ場合
ymax = ymax + stepVal; //y軸最大値に目盛間隔分を足す
}
//グラフの描画
new Chart(canvas.getContext('2d'), {
type: 'bar',
data: {
labels: <?php echo $json_year_month; ?>,
datasets: [{
data: <?php echo $json_amount; ?>,
backgroundColor: ['#28a7e0'],
datalabels: { //データラベル設定
color: '#28a7e0',
font: {
size: '11px',
weight: 'bold'
},
anchor: 'end', // データラベルの位置('end' は上端)
align: 'end',
formatter: function(value, context) {
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' 円'; // データラベルに文字などを付け足す
},
},
}],
},
plugins: [ChartDataLabels],
options: {
responsive: false,
scales: {
y: {
max: ymax,
ticks: {
stepSize: stepVal,
callback: function(tick) {
return tick.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' 円';
},
}
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
},
},
},
})
}
</script>
</body>
</html>
検索フォーム実装
まずは検索フォーム部分です。年を選択する部分と、支出と収入を切り替えるボタン、年間の支出や収入金額を出力する要素に修正します。
<form class=”p-form–bar-graph u-flex-box”>〜</form>を以下に書き換えます。
<form class="p-form--bar-graph all-report u-flex-box" action="" method="POST" name="graphSearch">
<input type="submit" name="prev" class="fas" value="">
<input type="hidden" name="year" value="2023-01" readonly>
<input type="text" name="showyear" value="2023年" readonly>
<input type="submit" name="next" class="fas" value="">
<div class="p-form__flex-input">
<input id="spending" type="radio" name="type" value="0" onchange="" required>
<label for="spending">支出</label>
<input type="radio" name="type" id="income" value="1" onchange="">
<label for="income">収入</label>
</div>
</form>
<div class="sum">
<p>
年間支出金額:
<span>¥100,000</span>
</p>
</div>
上記を書き換えると以下のように年や支出収支の選択項目、金額出力部分ができあがります。
続いてフォーム部分をPHP化します。
まずはPOST送信が行われた際にデータを受け取るプログラムをファイル冒頭に記述します。
「2023年」左右にある「<」「>」がtype=submitのボタンなので、押下するとPOST送信が行われます。
if (isset($_POST['prev'])) :
$type = filter_input(INPUT_POST, 'type', FILTER_SANITIZE_NUMBER_INT);
$now_year = filter_input(INPUT_POST, 'year', FILTER_SANITIZE_NUMBER_INT);
$base_year = strtotime($now_year);
$year = date('Y', strtotime('-1 year', $base_year));
elseif (isset($_POST['next'])) :
$type = filter_input(INPUT_POST, 'type', FILTER_SANITIZE_NUMBER_INT);
$now_year = filter_input(INPUT_POST, 'year', FILTER_SANITIZE_NUMBER_INT);
$base_year = strtotime($now_year);
$year = date('Y', strtotime('+1 year', $base_year));
else :
$type = '0';
$year = date('Y');
endif;
上記を追記したら月選択や収支選択フォームに送られてきたデータを表示するようにします。
<form class="p-form--bar-graph all-report u-flex-box" action="" method="POST" name="graphSearch">
<input type="submit" name="prev" class="fas" value="">
<!--以下2行のvalueの2023を送られてきたデータになるように修正-->
<input type="hidden" name="year" value="<?php echo $year; ?>-01" readonly>
<input type="text" name="showyear" value="<?php echo $year; ?>年" readonly>
<input type="submit" name="next" class="fas" value="">
<div class="p-form__flex-input">
<!--以下のinput要素2箇所に送られてきたタイプ(支出か収入)を判断してchecked属性を出力する処理追加-->
<input id="spending" type="radio" name="type" value="0" onchange="" required <?php echo $type === '0' ? 'checked' : ''; ?>>
<label for="spending">支出</label>
<input type="radio" name="type" id="income" value="1" onchange="" <?php echo $type === '1' ? 'checked' : ''; ?>>
<label for="income">収入</label>
</div>
</form>
フォーム部分が動くようになりました。
合計金額の計算と切り替え
続いて合計金額部分を支出収入によって切り替えるようにします。
切り替えるためにまずは、支出収入を押したらPOST送信がされるイベントを設定します。
以下のように「支出」「収入」のinputのonchange属性に「submit(this.form)」を設定します。
この設定をすることで、同じname属性のradioボタンが変更されるとPOST送信が行われるようになります。
<input id="spending" type="radio" name="type" value="0" onchange="submit(this.form)" required <?php echo $type === '0' ? 'checked' : ''; ?>>
<input type="radio" name="type" id="income" value="1" onchange="submit(this.form)" <?php echo $type === '1' ? 'checked' : ''; ?>>
続いて支出収入切り替えのPOST送信時にデータの受取プログラムを追記します。
このプログラムは、先ほど年切り替えのPOST送信時のデータ受け取りプログラムを記述した箇所にelseifを追加して記述します。
if (isset($_POST['prev'])) :
$type = filter_input(INPUT_POST, 'type', FILTER_SANITIZE_NUMBER_INT);
$now_year = filter_input(INPUT_POST, 'year', FILTER_SANITIZE_NUMBER_INT);
$base_year = strtotime($now_year);
$year = date('Y', strtotime('-1 year', $base_year));
elseif (isset($_POST['next'])) :
$type = filter_input(INPUT_POST, 'type', FILTER_SANITIZE_NUMBER_INT);
$now_year = filter_input(INPUT_POST, 'year', FILTER_SANITIZE_NUMBER_INT);
$base_year = strtotime($now_year);
$year = date('Y', strtotime('+1 year', $base_year));
//追加
elseif (isset($_POST['type'])) :
$type = filter_input(INPUT_POST, 'type', FILTER_SANITIZE_NUMBER_INT);
$now_year = filter_input(INPUT_POST, 'year', FILTER_SANITIZE_NUMBER_INT);
$base_year = strtotime($now_year);
$year = date('Y', $base_year);
//追加ここまで
else :
$type = '0';
$year = date('Y');
endif;
これで支出収入の切り替えでもPOST送信ができるようになりました。
それでは合計金額の切り替えを実装します。合計計算要素部分を以下に書き換えます。
<div class="sum">
<?php
$sql = 'SELECT SUM(amount) FROM records WHERE user_id=? AND type=? AND date LIKE ?';
$stmt = $db->prepare($sql);
$year_param = $year . '%';
$stmt->bind_param('iis', $user_id, $type, $year_param);
$stmt->execute();
$stmt->bind_result($amount_sum);
$stmt->fetch(); ?>
<p>
年間<?php echo $type === '0' ? '支出' : '収入'; ?>金額:
<span>¥<?php echo number_format($amount_sum); ?></span>
</p>
<?php $stmt->close(); ?>
</div>
以上のように書き換えることで合計計算の切り替えができるようになりました。
グラフの生成
続いてグラフデータを生成するためにSQLでデータを抽出しデータ配列を生成するプログラムを記述します。
以下を<div class=”bar-graph” id=”barGraph”>の下に追記します。
プログラムの流れは選択項目別ページ部分と同様です。
<?php
//すべての月が0円のデータ配列を生成
$month_list = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
$data = array();
for ($a = 0; $a < count($month_list); $a++) :
$data[] = [
'year_month' => $year . '-' . $month_list[$a],
'amount' => '0'
];
endfor;
//選択された年のデータを抽出
$sql = "SELECT LEFT(date, 7) as month, SUM(amount)
FROM records
WHERE user_id=? AND type=? AND date LIKE ?
GROUP BY month
ORDER BY month ASC";
$stmt = $db->prepare($sql);
$stmt->bind_param('iis', $user_id, $type, $year_param);
$stmt->execute();
$stmt->bind_result($month, $sum);
while ($stmt->fetch()) :
//取得したデータをデータ配列に再代入(1円以上のデータ月のみ)
$key = array_search($month, array_column($data, 'year_month'));
$data[$key]['amount'] = $sum;
endwhile;
//JSON形式に変換
$year_month = [];
$sum = [];
for ($i = 0; $i < count($month_list); $i++) :
$year_month[] = str_replace($year . '-', '', $data[$i]['year_month']) . '月';
$sum[] = $data[$i]['amount'];
endfor;
$json_month = json_encode($year_month);
$json_amount = json_encode($sum);
?>
以上でPHP部分のプログラムは完了です。ここからはPHPで生成したデータをJavaScriptに渡して、chart.jsを用いてグラフを描画するプログラムを記述します。
プログラムの内容は選択項目別ページのグラフ描画プログラムと同じなのですが、不要なプログラムと書き換える箇所が1点あります。
まずは「const canvas = document.getElementById(‘canvas’);」より上に記述されている以下のプログラム部分を削除します。
//以下を削除
const itemTable = document.getElementById('itemTable');
const selectForm = document.graphSearch.item;
//key:テーブル名、値:optgroupのlabelの連想配列
const tableList = {
'spending_category': '支出カテゴリー',
'income_category': '収入カテゴリー',
'creditcard': 'クレジットカード',
'qr': 'スマホ決済'
};
const onChangeItem = () => {
//選択されたoptionが何番目の要素か
const num = document.graphSearch.item.selectedIndex;
//選択されたoptionの親要素optgroupのラベル取得
const optGroupLabel = selectForm.options[num].parentNode.label;
//取得したoptgroupラベルと一致する連想配列の値を検索し、そのkey名を返す
const key = Object.keys(tableList).find((key) => tableList[key] === optGroupLabel);
//テーブル名を入れるinputのvalueを書き換え
itemTable.value = key;
}
そしてPHPで生成したデータJSON配列名が異なるのでそこを修正します。
「new Chart…」内の「data:{」のすぐ下にある「labels」の値を以下のように修正します。
//書き換え前
labels: <?php echo $json_year_month; ?>,
//書き換え後
labels: <?php echo $json_month; ?>,
以上を修正するとグラフが描画されます。
以上で年間レポートページは完成です。
最後に
今回は、ヘッダーメニューのハンバーガーメニューへの切り替えと、収入と支出の月別年間レポートページを実装しました。
家計簿アプリの機能開発は一旦ここで終了します。
次回以降はリファクタリングを行っていきます。
最後までお読みいただきありがとうございました。
コメント