家計簿アプリを作る #47:年間サマリーページ
家計簿アプリ作成シリーズの第47回です。これまでは月ごとの収支や推移は見られましたが、「1年を通してどうだったか」を一覧できる画面が無かったので、年単位の収支サマリーページを作ります。
実装方針
/summary ページを新規作成し、年を選択できる Select と、選択した年の月別収支テーブル・年間合計を表示します。既存の収支一覧ページの「月次推移」の集計ロジック(allTransactions を月ごとに filter/reduce)と考え方は同じで、集計の粒度を「年」に変える形です。
number と string の比較エラー
年の一覧を作る際、最初は date.getFullYear() の戻り値(number)をそのまま使っていました。
// ❌ numberの配列になってしまう
const uniqueYears = Array.from(
new Set(allTransactions.map((t) => new Date(t.date).getFullYear()).sort((a, b) => b - a))
);
一方、クエリパラメータから取得する selectedYear は string なので、+page.svelte 側で year === data.selectedYear と比較したときに
This comparison appears to be unintentional because the types 'number' and 'string' have no overlap.
という型エラーになりました。既存の months(月の一覧)が文字列で統一されているのに合わせて、uniqueYears も文字列に変換して解消しました。
// ✅ 文字列に統一する
const uniqueYears = Array.from(
new Set(allTransactions.map((t) => String(new Date(t.date).getFullYear())).sort((a, b) => (a > b ? -1 : 1)))
);
月別テーブルの位置づけ
年間サマリーに何を表示するか検討する中で、「月別の情報をテーブルで表示するのは既存の収支一覧ページのテーブルと同じでは?」という点を整理しました。
- 収支一覧ページのテーブル: 選択した月の中の、個々の取引を1件ずつ並べたもの
- 年間サマリーのテーブル: 選択した年の中で、月ごとに集計した合計値を1行ずつ並べたもの(最大12行)
粒度が違うので、両方あっても重複にはなりません。列は「年月・収入合計・支出合計・残高」の4列にしました。
const monthlyTransactions = Array.from(
new Set(yearTransactions.map((t) => {
const date = new Date(t.date);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
}))
).map(month => {
const monthTransactions = yearTransactions.filter(t => t.date.startsWith(month));
const monthIncome = monthTransactions.filter(t => t.type === 'income').reduce((sum, t) => sum + t.amount, 0);
const monthExpense = monthTransactions.filter(t => t.type === 'expense').reduce((sum, t) => sum + t.amount, 0);
return {
month,
totalMonthlyIncome: monthIncome,
totalMonthlyExpense: monthExpense,
totalMonthlyBalance: monthIncome - monthExpense,
};
});
まとめ
| ポイント | 内容 |
|---|---|
| 年の一覧の型 | 既存のmonthsに合わせて文字列で統一する |
| 月別テーブルの意義 | 収支一覧の取引明細とは粒度が異なるので重複しない |
| テーブルの列構成 | 年月・収入合計・支出合計・残高の4列で十分 |